@truenas/ui-components 0.3.6 → 0.3.8
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.
|
@@ -1760,6 +1760,23 @@ class TnIconComponent {
|
|
|
1760
1760
|
}
|
|
1761
1761
|
return null;
|
|
1762
1762
|
}, ...(ngDevMode ? [{ debugName: "hostDimension" }] : []));
|
|
1763
|
+
/**
|
|
1764
|
+
* Inline min-width/min-height override for the custom/full-size paths.
|
|
1765
|
+
* - customSize: pin the minimum to the custom value so flex layouts can't
|
|
1766
|
+
* squish it (mirrors the preset min-* rules in the stylesheet).
|
|
1767
|
+
* - fullSize: relax the minimum to 0 so the icon can shrink with its
|
|
1768
|
+
* container instead of being clamped by the `size` attribute's preset min.
|
|
1769
|
+
*/
|
|
1770
|
+
hostMinDimension = computed(() => {
|
|
1771
|
+
const custom = this.customSize();
|
|
1772
|
+
if (custom) {
|
|
1773
|
+
return custom;
|
|
1774
|
+
}
|
|
1775
|
+
if (this.fullSize()) {
|
|
1776
|
+
return '0';
|
|
1777
|
+
}
|
|
1778
|
+
return null;
|
|
1779
|
+
}, ...(ngDevMode ? [{ debugName: "hostMinDimension" }] : []));
|
|
1763
1780
|
hostFontSize = computed(() => {
|
|
1764
1781
|
const custom = this.customSize();
|
|
1765
1782
|
if (custom) {
|
|
@@ -1951,7 +1968,7 @@ class TnIconComponent {
|
|
|
1951
1968
|
`To include it, add ${markerExample} to your source and run 'npx truenas-icons generate'.`);
|
|
1952
1969
|
}
|
|
1953
1970
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1954
|
-
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 }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, fullSize: { classPropertyName: "fullSize", publicName: "fullSize", isSignal: true, isRequired: false, transformFunction: null }, customSize: { classPropertyName: "customSize", publicName: "customSize", 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", "attr.custom-size": "customSize() || null", "style.width": "hostDimension()", "style.height": "hostDimension()", "style.font-size": "hostFontSize()", "style.color": "color() || null" } }, viewQueries: [{ propertyName: "svgContainer", first: true, predicate: ["svgContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n tnTestIdType=\"icon\"\n [tnTestId]=\"testId()\"\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;color:var(--tn-icon-color, currentColor);width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));font-size:var(--tn-icon-size, var(--tn-icon-md))}tn-icon[size=xs]{width:var(--tn-icon-xs);height:var(--tn-icon-xs);font-size:var(--tn-icon-xs)}tn-icon[size=sm]{width:var(--tn-icon-sm);height:var(--tn-icon-sm);font-size:var(--tn-icon-sm)}tn-icon[size=md]{width:var(--tn-icon-md);height:var(--tn-icon-md);font-size:var(--tn-icon-md)}tn-icon[size=lg]{width:var(--tn-icon-lg);height:var(--tn-icon-lg);font-size:var(--tn-icon-lg)}tn-icon[size=xl]{width:var(--tn-icon-xl);height:var(--tn-icon-xl);font-size:var(--tn-icon-xl)}.tn-icon{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.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}\n"], dependencies: [{ kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1971
|
+
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 }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, fullSize: { classPropertyName: "fullSize", publicName: "fullSize", isSignal: true, isRequired: false, transformFunction: null }, customSize: { classPropertyName: "customSize", publicName: "customSize", 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", "attr.custom-size": "customSize() || null", "style.width": "hostDimension()", "style.height": "hostDimension()", "style.min-width": "hostMinDimension()", "style.min-height": "hostMinDimension()", "style.font-size": "hostFontSize()", "style.color": "color() || null" } }, viewQueries: [{ propertyName: "svgContainer", first: true, predicate: ["svgContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n tnTestIdType=\"icon\"\n [tnTestId]=\"testId()\"\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;color:var(--tn-icon-color, currentColor);width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));min-width:var(--tn-icon-size, var(--tn-icon-md));min-height:var(--tn-icon-size, var(--tn-icon-md));font-size:var(--tn-icon-size, var(--tn-icon-md))}tn-icon[size=xs]{width:var(--tn-icon-xs);height:var(--tn-icon-xs);min-width:var(--tn-icon-xs);min-height:var(--tn-icon-xs);font-size:var(--tn-icon-xs)}tn-icon[size=sm]{width:var(--tn-icon-sm);height:var(--tn-icon-sm);min-width:var(--tn-icon-sm);min-height:var(--tn-icon-sm);font-size:var(--tn-icon-sm)}tn-icon[size=md]{width:var(--tn-icon-md);height:var(--tn-icon-md);min-width:var(--tn-icon-md);min-height:var(--tn-icon-md);font-size:var(--tn-icon-md)}tn-icon[size=lg]{width:var(--tn-icon-lg);height:var(--tn-icon-lg);min-width:var(--tn-icon-lg);min-height:var(--tn-icon-lg);font-size:var(--tn-icon-lg)}tn-icon[size=xl]{width:var(--tn-icon-xl);height:var(--tn-icon-xl);min-width:var(--tn-icon-xl);min-height:var(--tn-icon-xl);font-size:var(--tn-icon-xl)}.tn-icon{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.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}\n"], dependencies: [{ kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1955
1972
|
}
|
|
1956
1973
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnIconComponent, decorators: [{
|
|
1957
1974
|
type: Component,
|
|
@@ -1964,9 +1981,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
1964
1981
|
'[attr.custom-size]': 'customSize() || null',
|
|
1965
1982
|
'[style.width]': 'hostDimension()',
|
|
1966
1983
|
'[style.height]': 'hostDimension()',
|
|
1984
|
+
'[style.min-width]': 'hostMinDimension()',
|
|
1985
|
+
'[style.min-height]': 'hostMinDimension()',
|
|
1967
1986
|
'[style.font-size]': 'hostFontSize()',
|
|
1968
1987
|
'[style.color]': 'color() || null',
|
|
1969
|
-
}, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n tnTestIdType=\"icon\"\n [tnTestId]=\"testId()\"\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;color:var(--tn-icon-color, currentColor);width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));font-size:var(--tn-icon-size, var(--tn-icon-md))}tn-icon[size=xs]{width:var(--tn-icon-xs);height:var(--tn-icon-xs);font-size:var(--tn-icon-xs)}tn-icon[size=sm]{width:var(--tn-icon-sm);height:var(--tn-icon-sm);font-size:var(--tn-icon-sm)}tn-icon[size=md]{width:var(--tn-icon-md);height:var(--tn-icon-md);font-size:var(--tn-icon-md)}tn-icon[size=lg]{width:var(--tn-icon-lg);height:var(--tn-icon-lg);font-size:var(--tn-icon-lg)}tn-icon[size=xl]{width:var(--tn-icon-xl);height:var(--tn-icon-xl);font-size:var(--tn-icon-xl)}.tn-icon{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.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}\n"] }]
|
|
1988
|
+
}, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n tnTestIdType=\"icon\"\n [tnTestId]=\"testId()\"\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;color:var(--tn-icon-color, currentColor);width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));min-width:var(--tn-icon-size, var(--tn-icon-md));min-height:var(--tn-icon-size, var(--tn-icon-md));font-size:var(--tn-icon-size, var(--tn-icon-md))}tn-icon[size=xs]{width:var(--tn-icon-xs);height:var(--tn-icon-xs);min-width:var(--tn-icon-xs);min-height:var(--tn-icon-xs);font-size:var(--tn-icon-xs)}tn-icon[size=sm]{width:var(--tn-icon-sm);height:var(--tn-icon-sm);min-width:var(--tn-icon-sm);min-height:var(--tn-icon-sm);font-size:var(--tn-icon-sm)}tn-icon[size=md]{width:var(--tn-icon-md);height:var(--tn-icon-md);min-width:var(--tn-icon-md);min-height:var(--tn-icon-md);font-size:var(--tn-icon-md)}tn-icon[size=lg]{width:var(--tn-icon-lg);height:var(--tn-icon-lg);min-width:var(--tn-icon-lg);min-height:var(--tn-icon-lg);font-size:var(--tn-icon-lg)}tn-icon[size=xl]{width:var(--tn-icon-xl);height:var(--tn-icon-xl);min-width:var(--tn-icon-xl);min-height:var(--tn-icon-xl);font-size:var(--tn-icon-xl)}.tn-icon{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.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}\n"] }]
|
|
1970
1989
|
}], 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 }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], fullSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullSize", required: false }] }], customSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "customSize", required: false }] }], svgContainer: [{ type: i0.ViewChild, args: ['svgContainer', { isSignal: true }] }] } });
|
|
1971
1990
|
|
|
1972
1991
|
/**
|
|
@@ -4375,20 +4394,37 @@ let nextId = 0;
|
|
|
4375
4394
|
* committed to a chip on Enter (or a configurable separator key); Backspace on
|
|
4376
4395
|
* an empty field removes the last chip.
|
|
4377
4396
|
*
|
|
4378
|
-
* It is a `ControlValueAccessor` over `string[]
|
|
4379
|
-
* or template-driven form (`[formControl]`, `[(ngModel)]`)
|
|
4380
|
-
* `tn-form-field` as a real projected control — the field's
|
|
4381
|
-
* inference reads this control directly.
|
|
4397
|
+
* It is a `ControlValueAccessor` over `T[]` (defaulting to `string[]`), so it
|
|
4398
|
+
* drops into a reactive or template-driven form (`[formControl]`, `[(ngModel)]`)
|
|
4399
|
+
* and slots into a `tn-form-field` as a real projected control — the field's
|
|
4400
|
+
* required/error inference reads this control directly.
|
|
4401
|
+
*
|
|
4402
|
+
* **String mode (default).** Pass `[suggestions]` (a `string[]`) for typeahead;
|
|
4403
|
+
* the typed/picked string is the value. Set `allowCustomValue=false` to restrict
|
|
4404
|
+
* commits to the suggestion list.
|
|
4382
4405
|
*
|
|
4383
|
-
*
|
|
4384
|
-
*
|
|
4385
|
-
*
|
|
4386
|
-
*
|
|
4406
|
+
* **Value mode.** Pass `[options]` (`{ label, value }[]`) to display labels while
|
|
4407
|
+
* committing values — the model becomes `T[]`. A written value resolves to its
|
|
4408
|
+
* option's label (falling back to `String(value)` until the option is
|
|
4409
|
+
* available). Provide `[compareWith]` when the values are objects. Committing a
|
|
4410
|
+
* typed string matches an option by label (case-insensitive); free text that
|
|
4411
|
+
* matches no option is only accepted when `allowCustomValue` is `true` (which is
|
|
4412
|
+
* only sound for string-valued inputs).
|
|
4413
|
+
*
|
|
4414
|
+
* Either source can be driven asynchronously by listening to `(searchChange)`
|
|
4415
|
+
* and updating `[suggestions]`/`[options]` as results arrive. The dropdown is
|
|
4416
|
+
* portaled through a CDK overlay so it escapes any ancestor `overflow: hidden`.
|
|
4387
4417
|
*
|
|
4388
4418
|
* @example
|
|
4389
4419
|
* ```html
|
|
4420
|
+
* <!-- string mode -->
|
|
4390
4421
|
* <tn-form-field label="Tags">
|
|
4391
|
-
* <tn-chip-input [formControl]="tags"
|
|
4422
|
+
* <tn-chip-input [formControl]="tags" [suggestions]="tagSuggestions" />
|
|
4423
|
+
* </tn-form-field>
|
|
4424
|
+
*
|
|
4425
|
+
* <!-- value mode: shows names, commits ids -->
|
|
4426
|
+
* <tn-form-field label="Groups">
|
|
4427
|
+
* <tn-chip-input [formControl]="groupIds" [options]="groupOptions" [allowCustomValue]="false" />
|
|
4392
4428
|
* </tn-form-field>
|
|
4393
4429
|
* ```
|
|
4394
4430
|
*/
|
|
@@ -4410,30 +4446,42 @@ class TnChipInputComponent {
|
|
|
4410
4446
|
/** Commit a pending (non-empty) text value as a chip when the field loses focus. */
|
|
4411
4447
|
addOnBlur = input(false, ...(ngDevMode ? [{ debugName: "addOnBlur" }] : []));
|
|
4412
4448
|
/**
|
|
4413
|
-
* Whether free text not
|
|
4414
|
-
* `true` — any typed value becomes a chip. Set `false` to restrict
|
|
4415
|
-
* to its
|
|
4416
|
-
* succeeds when the text matches
|
|
4417
|
-
* the
|
|
4418
|
-
* `tn-autocomplete`'s `allowCustomValue`.
|
|
4419
|
-
*
|
|
4449
|
+
* Whether free text not matching any option/suggestion may be committed.
|
|
4450
|
+
* Defaults to `true` — any typed value becomes a chip. Set `false` to restrict
|
|
4451
|
+
* the field to its list (a "pick from the list" control): a commit only
|
|
4452
|
+
* succeeds when the text matches an option/suggestion label (case-insensitive,
|
|
4453
|
+
* committing the canonical entry); unmatched text is discarded. Mirrors
|
|
4454
|
+
* `tn-autocomplete`'s `allowCustomValue`. In value mode, leave this `false` —
|
|
4455
|
+
* fabricating a typed string as a non-string value is unsound.
|
|
4420
4456
|
*/
|
|
4421
4457
|
allowCustomValue = input(true, ...(ngDevMode ? [{ debugName: "allowCustomValue" }] : []));
|
|
4422
4458
|
/**
|
|
4423
4459
|
* Allow the same value to be added more than once. Off by default.
|
|
4424
|
-
* Duplicate detection
|
|
4425
|
-
* `Angular` and `angular` are
|
|
4426
|
-
* *filtering* is case-insensitive.
|
|
4460
|
+
* Duplicate detection uses `compareWith` (or identity); string-mode matching
|
|
4461
|
+
* is exact (case-sensitive), so `Angular` and `angular` are distinct — only
|
|
4462
|
+
* the *filtering* of suggestions is case-insensitive.
|
|
4427
4463
|
*/
|
|
4428
4464
|
allowDuplicates = input(false, ...(ngDevMode ? [{ debugName: "allowDuplicates" }] : []));
|
|
4429
4465
|
/** Hard cap on the number of chips; `undefined` means no limit. */
|
|
4430
4466
|
maxChips = input(undefined, ...(ngDevMode ? [{ debugName: "maxChips" }] : []));
|
|
4431
4467
|
/**
|
|
4432
|
-
*
|
|
4433
|
-
* match the typed text and are not already selected.
|
|
4434
|
-
* update this in response to `(searchChange)`.
|
|
4468
|
+
* String-mode suggestion list. When non-empty, a dropdown offers entries that
|
|
4469
|
+
* match the typed text and are not already selected. Ignored when `options`
|
|
4470
|
+
* is provided. For async sources, update this in response to `(searchChange)`.
|
|
4435
4471
|
*/
|
|
4436
4472
|
suggestions = input([], ...(ngDevMode ? [{ debugName: "suggestions" }] : []));
|
|
4473
|
+
/**
|
|
4474
|
+
* Value-mode option list (`{ label, value }`). When non-empty, chips display
|
|
4475
|
+
* the resolved `label` while the form model holds `value`s. Takes precedence
|
|
4476
|
+
* over `suggestions`. For async sources, update in response to `(searchChange)`.
|
|
4477
|
+
*/
|
|
4478
|
+
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
4479
|
+
/**
|
|
4480
|
+
* Comparator for value equality — used for de-duplication, display resolution
|
|
4481
|
+
* and the selected-set. Defaults to identity (`===`), correct for primitives;
|
|
4482
|
+
* provide this when values are objects (e.g. `(a, b) => a?.id === b?.id`).
|
|
4483
|
+
*/
|
|
4484
|
+
compareWith = input(undefined, ...(ngDevMode ? [{ debugName: "compareWith" }] : []));
|
|
4437
4485
|
/** Accessible name for the text field. Leave unset inside a `tn-form-field`. */
|
|
4438
4486
|
ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
4439
4487
|
/**
|
|
@@ -4474,15 +4522,25 @@ class TnChipInputComponent {
|
|
|
4474
4522
|
const max = this.maxChips();
|
|
4475
4523
|
return max === undefined || this.values().length < max;
|
|
4476
4524
|
}, ...(ngDevMode ? [{ debugName: "canAddMore" }] : []));
|
|
4477
|
-
/**
|
|
4525
|
+
/**
|
|
4526
|
+
* Unified option list. Value-mode `options` win; otherwise string-mode
|
|
4527
|
+
* `suggestions` are lifted into `{ label: s, value: s }`.
|
|
4528
|
+
*/
|
|
4529
|
+
optionList = computed(() => {
|
|
4530
|
+
const opts = this.options();
|
|
4531
|
+
if (opts.length) {
|
|
4532
|
+
return opts;
|
|
4533
|
+
}
|
|
4534
|
+
return this.suggestions().map((suggestion) => ({ label: suggestion, value: suggestion }));
|
|
4535
|
+
}, ...(ngDevMode ? [{ debugName: "optionList" }] : []));
|
|
4536
|
+
/** Options matching the typed text and not already selected. */
|
|
4478
4537
|
filteredSuggestions = computed(() => {
|
|
4479
|
-
const selected = new Set(this.values());
|
|
4480
4538
|
const term = this.inputValue().trim().toLowerCase();
|
|
4481
|
-
return this.
|
|
4482
|
-
if (
|
|
4539
|
+
return this.optionList().filter((option) => {
|
|
4540
|
+
if (this.valuesIncludes(option.value)) {
|
|
4483
4541
|
return false;
|
|
4484
4542
|
}
|
|
4485
|
-
return term === '' ||
|
|
4543
|
+
return term === '' || option.label.toLowerCase().includes(term);
|
|
4486
4544
|
});
|
|
4487
4545
|
}, ...(ngDevMode ? [{ debugName: "filteredSuggestions" }] : []));
|
|
4488
4546
|
onChange = () => { };
|
|
@@ -4492,10 +4550,10 @@ class TnChipInputComponent {
|
|
|
4492
4550
|
constructor() {
|
|
4493
4551
|
// Async suggestions: when the user types, onInput runs syncDropdown()
|
|
4494
4552
|
// against the still-stale list and leaves the panel closed; results land a
|
|
4495
|
-
// tick later via [suggestions]. Re-open the panel once fresh
|
|
4496
|
-
// while the field is focused and actively searching. This
|
|
4497
|
-
// (never closes), so it doesn't fight Escape, blur, or the
|
|
4498
|
-
// — those stay shut until the
|
|
4553
|
+
// tick later via [suggestions]/[options]. Re-open the panel once fresh
|
|
4554
|
+
// matches arrive while the field is focused and actively searching. This
|
|
4555
|
+
// only ever opens (never closes), so it doesn't fight Escape, blur, or the
|
|
4556
|
+
// post-commit close — those stay shut until the option set next changes.
|
|
4499
4557
|
effect(() => {
|
|
4500
4558
|
const hasMatches = this.filteredSuggestions().length > 0;
|
|
4501
4559
|
untracked(() => {
|
|
@@ -4552,7 +4610,7 @@ class TnChipInputComponent {
|
|
|
4552
4610
|
onBlur() {
|
|
4553
4611
|
this.focused.set(false);
|
|
4554
4612
|
if (this.addOnBlur()) {
|
|
4555
|
-
this.
|
|
4613
|
+
this.commitText(this.inputValue());
|
|
4556
4614
|
}
|
|
4557
4615
|
this.close();
|
|
4558
4616
|
this.onTouched();
|
|
@@ -4594,10 +4652,10 @@ class TnChipInputComponent {
|
|
|
4594
4652
|
event.preventDefault();
|
|
4595
4653
|
const idx = this.highlightedIndex();
|
|
4596
4654
|
if (this.isOpen() && idx >= 0 && idx < suggestions.length) {
|
|
4597
|
-
this.
|
|
4655
|
+
this.commitValue(suggestions[idx].value);
|
|
4598
4656
|
}
|
|
4599
4657
|
else {
|
|
4600
|
-
this.
|
|
4658
|
+
this.commitText(this.inputValue());
|
|
4601
4659
|
}
|
|
4602
4660
|
return;
|
|
4603
4661
|
}
|
|
@@ -4608,8 +4666,8 @@ class TnChipInputComponent {
|
|
|
4608
4666
|
this.removeChip(this.values().length - 1);
|
|
4609
4667
|
}
|
|
4610
4668
|
}
|
|
4611
|
-
onSuggestionClick(
|
|
4612
|
-
this.
|
|
4669
|
+
onSuggestionClick(option) {
|
|
4670
|
+
this.commitValue(option.value);
|
|
4613
4671
|
this.inputEl().nativeElement.focus();
|
|
4614
4672
|
}
|
|
4615
4673
|
/** Prevents the option `mousedown` from blurring the input before the click lands. */
|
|
@@ -4621,7 +4679,7 @@ class TnChipInputComponent {
|
|
|
4621
4679
|
return;
|
|
4622
4680
|
}
|
|
4623
4681
|
const removed = this.values()[index];
|
|
4624
|
-
if (
|
|
4682
|
+
if (index < 0 || index >= this.values().length) {
|
|
4625
4683
|
return;
|
|
4626
4684
|
}
|
|
4627
4685
|
this.values.update((values) => values.filter((_, i) => i !== index));
|
|
@@ -4634,6 +4692,11 @@ class TnChipInputComponent {
|
|
|
4634
4692
|
this.inputEl().nativeElement.focus();
|
|
4635
4693
|
this.syncDropdown();
|
|
4636
4694
|
}
|
|
4695
|
+
/** The label shown on a chip for a committed value. */
|
|
4696
|
+
displayLabel(value) {
|
|
4697
|
+
const match = this.optionList().find((option) => this.valueMatches(option.value, value));
|
|
4698
|
+
return match ? match.label : String(value);
|
|
4699
|
+
}
|
|
4637
4700
|
/** Scopes a per-chip test id beneath the component's base. */
|
|
4638
4701
|
chipTestId(value) {
|
|
4639
4702
|
const base = this.testId();
|
|
@@ -4646,22 +4709,29 @@ class TnChipInputComponent {
|
|
|
4646
4709
|
isCommitKey(event) {
|
|
4647
4710
|
return event.key === 'Enter' || this.separatorKeys().includes(event.key);
|
|
4648
4711
|
}
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4712
|
+
/** Commits typed text: resolve it to an option's value, else accept as custom. */
|
|
4713
|
+
commitText(raw) {
|
|
4714
|
+
const text = (raw ?? '').trim();
|
|
4715
|
+
if (!text) {
|
|
4652
4716
|
return;
|
|
4653
4717
|
}
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4718
|
+
const match = this.optionList().find((option) => option.label.toLowerCase() === text.toLowerCase());
|
|
4719
|
+
if (match) {
|
|
4720
|
+
this.commitValue(match.value);
|
|
4721
|
+
return;
|
|
4722
|
+
}
|
|
4723
|
+
if (this.allowCustomValue()) {
|
|
4724
|
+
this.commitValue(text);
|
|
4725
|
+
return;
|
|
4726
|
+
}
|
|
4727
|
+
this.clearInput();
|
|
4728
|
+
}
|
|
4729
|
+
/** Commits a resolved value, honouring duplicate and cap rules. */
|
|
4730
|
+
commitValue(value) {
|
|
4731
|
+
if (this.isDisabled() || !this.canAddMore()) {
|
|
4732
|
+
return;
|
|
4663
4733
|
}
|
|
4664
|
-
if (!this.allowDuplicates() && this.
|
|
4734
|
+
if (!this.allowDuplicates() && this.valuesIncludes(value)) {
|
|
4665
4735
|
this.clearInput();
|
|
4666
4736
|
return;
|
|
4667
4737
|
}
|
|
@@ -4671,6 +4741,13 @@ class TnChipInputComponent {
|
|
|
4671
4741
|
this.chipAdded.emit(value);
|
|
4672
4742
|
this.clearInput();
|
|
4673
4743
|
}
|
|
4744
|
+
valuesIncludes(value) {
|
|
4745
|
+
return this.values().some((existing) => this.valueMatches(existing, value));
|
|
4746
|
+
}
|
|
4747
|
+
valueMatches(a, b) {
|
|
4748
|
+
const comparator = this.compareWith();
|
|
4749
|
+
return comparator ? comparator(a, b) : a === b;
|
|
4750
|
+
}
|
|
4674
4751
|
clearInput() {
|
|
4675
4752
|
this.inputValue.set('');
|
|
4676
4753
|
this.inputEl().nativeElement.value = '';
|
|
@@ -4679,7 +4756,7 @@ class TnChipInputComponent {
|
|
|
4679
4756
|
/**
|
|
4680
4757
|
* Opens the dropdown when there is something to show, closes it otherwise.
|
|
4681
4758
|
* Stays closed once the chip cap is reached — suggesting rows that
|
|
4682
|
-
* `
|
|
4759
|
+
* `commitValue()` would reject is misleading.
|
|
4683
4760
|
*/
|
|
4684
4761
|
syncDropdown() {
|
|
4685
4762
|
if (this.filteredSuggestions().length > 0 && this.canAddMore() && !this.isDisabled()) {
|
|
@@ -4739,13 +4816,13 @@ class TnChipInputComponent {
|
|
|
4739
4816
|
this.overlayRef = undefined;
|
|
4740
4817
|
}
|
|
4741
4818
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnChipInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4742
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnChipInputComponent, isStandalone: true, selector: "tn-chip-input", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, separatorKeys: { classPropertyName: "separatorKeys", publicName: "separatorKeys", isSignal: true, isRequired: false, transformFunction: null }, addOnBlur: { classPropertyName: "addOnBlur", publicName: "addOnBlur", isSignal: true, isRequired: false, transformFunction: null }, allowCustomValue: { classPropertyName: "allowCustomValue", publicName: "allowCustomValue", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, maxChips: { classPropertyName: "maxChips", publicName: "maxChips", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { chipAdded: "chipAdded", chipRemoved: "chipRemoved", searchChange: "searchChange" }, providers: [
|
|
4819
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnChipInputComponent, isStandalone: true, selector: "tn-chip-input", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, separatorKeys: { classPropertyName: "separatorKeys", publicName: "separatorKeys", isSignal: true, isRequired: false, transformFunction: null }, addOnBlur: { classPropertyName: "addOnBlur", publicName: "addOnBlur", isSignal: true, isRequired: false, transformFunction: null }, allowCustomValue: { classPropertyName: "allowCustomValue", publicName: "allowCustomValue", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, maxChips: { classPropertyName: "maxChips", publicName: "maxChips", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { chipAdded: "chipAdded", chipRemoved: "chipRemoved", searchChange: "searchChange" }, providers: [
|
|
4743
4820
|
{
|
|
4744
4821
|
provide: NG_VALUE_ACCESSOR,
|
|
4745
4822
|
useExisting: forwardRef(() => TnChipInputComponent),
|
|
4746
4823
|
multi: true,
|
|
4747
4824
|
},
|
|
4748
|
-
], viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }, { propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Click-to-focus is a pointer affordance only; keyboard users Tab straight to\n the real <input> inside, so the container itself is not a focus target. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #container\n class=\"tn-chip-input\"\n [class.tn-chip-input--disabled]=\"isDisabled()\"\n (click)=\"focusInput()\"\n>\n @for (value of values(); track $index) {\n <tn-chip\n class=\"tn-chip-input__chip\"\n color=\"secondary\"\n [label]=\"value\"\n [closable]=\"!isDisabled()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"chipTestId(value)\"\n (onClose)=\"removeChip($index)\"\n />\n }\n\n <input\n #inputEl\n type=\"text\"\n class=\"tn-chip-input__field\"\n role=\"combobox\"\n autocomplete=\"off\"\n tnTestIdType=\"chip-input\"\n [tnTestId]=\"testId()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"isOpen() ? uid + '-listbox' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n />\n</div>\n\n<!-- Suggestion dropdown \u2014 portaled via CDK overlay so it escapes ancestor\n overflow/clipping. Attach/detach is driven by open()/close(). -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-chip-input__dropdown\"\n role=\"listbox\"\n [attr.id]=\"uid + '-listbox'\"\n >\n @for (suggestion of filteredSuggestions(); track
|
|
4825
|
+
], viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }, { propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Click-to-focus is a pointer affordance only; keyboard users Tab straight to\n the real <input> inside, so the container itself is not a focus target. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #container\n class=\"tn-chip-input\"\n [class.tn-chip-input--disabled]=\"isDisabled()\"\n (click)=\"focusInput()\"\n>\n @for (value of values(); track $index) {\n <tn-chip\n class=\"tn-chip-input__chip\"\n color=\"secondary\"\n [label]=\"displayLabel(value)\"\n [closable]=\"!isDisabled()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"chipTestId(value)\"\n (onClose)=\"removeChip($index)\"\n />\n }\n\n <input\n #inputEl\n type=\"text\"\n class=\"tn-chip-input__field\"\n role=\"combobox\"\n autocomplete=\"off\"\n tnTestIdType=\"chip-input\"\n [tnTestId]=\"testId()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"isOpen() ? uid + '-listbox' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n />\n</div>\n\n<!-- Suggestion dropdown \u2014 portaled via CDK overlay so it escapes ancestor\n overflow/clipping. Attach/detach is driven by open()/close(). -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-chip-input__dropdown\"\n role=\"listbox\"\n [attr.id]=\"uid + '-listbox'\"\n >\n @for (suggestion of filteredSuggestions(); track $index) {\n <!-- Options are tabindex=\"-1\" and never focused: keyboard selection goes\n through the input's aria-activedescendant + the Enter branch in\n onKeydown, so a key handler here would be dead code. Click is a\n pointer-only affordance. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-chip-input__option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"chipTestId(suggestion.value)\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"onSuggestionMousedown($event)\"\n (click)=\"onSuggestionClick(suggestion)\"\n >\n {{ suggestion.label }}\n </div>\n }\n </div>\n</ng-template>\n", styles: [".tn-chip-input{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;min-height:2.5rem;padding:.375rem .5rem;box-sizing:border-box;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:text;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-chip-input:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-chip-input--disabled{background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-chip-input__chip{flex-shrink:0}.tn-chip-input__field{flex:1;min-width:120px;padding:.25rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;outline:none}.tn-chip-input__field::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-chip-input__field:disabled{cursor:not-allowed}.tn-chip-input__dropdown{max-height:var(--tn-chip-input-panel-max-height, 320px);overflow-y:auto;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.tn-chip-input__option{padding:.5rem .75rem;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;font-size:1rem;color:var(--tn-fg1, #212529);cursor:pointer}.tn-chip-input__option:hover,.tn-chip-input__option.highlighted{background-color:var(--tn-bg3, #f3f4f6)}@media(prefers-reduced-motion:reduce){.tn-chip-input{transition:none}}@media(prefers-contrast:high){.tn-chip-input{border-width:2px}}\n"], dependencies: [{ kind: "component", type: TnChipComponent, selector: "tn-chip", inputs: ["label", "icon", "closable", "disabled", "color", "testId"], outputs: ["onClose", "onClick"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
|
|
4749
4826
|
}
|
|
4750
4827
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnChipInputComponent, decorators: [{
|
|
4751
4828
|
type: Component,
|
|
@@ -4755,8 +4832,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
4755
4832
|
useExisting: forwardRef(() => TnChipInputComponent),
|
|
4756
4833
|
multi: true,
|
|
4757
4834
|
},
|
|
4758
|
-
], template: "<!-- Click-to-focus is a pointer affordance only; keyboard users Tab straight to\n the real <input> inside, so the container itself is not a focus target. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #container\n class=\"tn-chip-input\"\n [class.tn-chip-input--disabled]=\"isDisabled()\"\n (click)=\"focusInput()\"\n>\n @for (value of values(); track $index) {\n <tn-chip\n class=\"tn-chip-input__chip\"\n color=\"secondary\"\n [label]=\"value\"\n [closable]=\"!isDisabled()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"chipTestId(value)\"\n (onClose)=\"removeChip($index)\"\n />\n }\n\n <input\n #inputEl\n type=\"text\"\n class=\"tn-chip-input__field\"\n role=\"combobox\"\n autocomplete=\"off\"\n tnTestIdType=\"chip-input\"\n [tnTestId]=\"testId()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"isOpen() ? uid + '-listbox' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n />\n</div>\n\n<!-- Suggestion dropdown \u2014 portaled via CDK overlay so it escapes ancestor\n overflow/clipping. Attach/detach is driven by open()/close(). -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-chip-input__dropdown\"\n role=\"listbox\"\n [attr.id]=\"uid + '-listbox'\"\n >\n @for (suggestion of filteredSuggestions(); track
|
|
4759
|
-
}], ctorParameters: () => [], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], separatorKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorKeys", required: false }] }], addOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "addOnBlur", required: false }] }], allowCustomValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomValue", required: false }] }], allowDuplicates: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDuplicates", required: false }] }], maxChips: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxChips", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], chipAdded: [{ type: i0.Output, args: ["chipAdded"] }], chipRemoved: [{ type: i0.Output, args: ["chipRemoved"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], container: [{ type: i0.ViewChild, args: ['container', { isSignal: true }] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
|
|
4835
|
+
], template: "<!-- Click-to-focus is a pointer affordance only; keyboard users Tab straight to\n the real <input> inside, so the container itself is not a focus target. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #container\n class=\"tn-chip-input\"\n [class.tn-chip-input--disabled]=\"isDisabled()\"\n (click)=\"focusInput()\"\n>\n @for (value of values(); track $index) {\n <tn-chip\n class=\"tn-chip-input__chip\"\n color=\"secondary\"\n [label]=\"displayLabel(value)\"\n [closable]=\"!isDisabled()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"chipTestId(value)\"\n (onClose)=\"removeChip($index)\"\n />\n }\n\n <input\n #inputEl\n type=\"text\"\n class=\"tn-chip-input__field\"\n role=\"combobox\"\n autocomplete=\"off\"\n tnTestIdType=\"chip-input\"\n [tnTestId]=\"testId()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"isOpen() ? uid + '-listbox' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n />\n</div>\n\n<!-- Suggestion dropdown \u2014 portaled via CDK overlay so it escapes ancestor\n overflow/clipping. Attach/detach is driven by open()/close(). -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-chip-input__dropdown\"\n role=\"listbox\"\n [attr.id]=\"uid + '-listbox'\"\n >\n @for (suggestion of filteredSuggestions(); track $index) {\n <!-- Options are tabindex=\"-1\" and never focused: keyboard selection goes\n through the input's aria-activedescendant + the Enter branch in\n onKeydown, so a key handler here would be dead code. Click is a\n pointer-only affordance. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-chip-input__option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"chipTestId(suggestion.value)\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"onSuggestionMousedown($event)\"\n (click)=\"onSuggestionClick(suggestion)\"\n >\n {{ suggestion.label }}\n </div>\n }\n </div>\n</ng-template>\n", styles: [".tn-chip-input{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;min-height:2.5rem;padding:.375rem .5rem;box-sizing:border-box;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:text;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-chip-input:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-chip-input--disabled{background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-chip-input__chip{flex-shrink:0}.tn-chip-input__field{flex:1;min-width:120px;padding:.25rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;outline:none}.tn-chip-input__field::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-chip-input__field:disabled{cursor:not-allowed}.tn-chip-input__dropdown{max-height:var(--tn-chip-input-panel-max-height, 320px);overflow-y:auto;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.tn-chip-input__option{padding:.5rem .75rem;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;font-size:1rem;color:var(--tn-fg1, #212529);cursor:pointer}.tn-chip-input__option:hover,.tn-chip-input__option.highlighted{background-color:var(--tn-bg3, #f3f4f6)}@media(prefers-reduced-motion:reduce){.tn-chip-input{transition:none}}@media(prefers-contrast:high){.tn-chip-input{border-width:2px}}\n"] }]
|
|
4836
|
+
}], ctorParameters: () => [], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], separatorKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorKeys", required: false }] }], addOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "addOnBlur", required: false }] }], allowCustomValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomValue", required: false }] }], allowDuplicates: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDuplicates", required: false }] }], maxChips: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxChips", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], chipAdded: [{ type: i0.Output, args: ["chipAdded"] }], chipRemoved: [{ type: i0.Output, args: ["chipRemoved"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], container: [{ type: i0.ViewChild, args: ['container', { isSignal: true }] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
|
|
4760
4837
|
|
|
4761
4838
|
/**
|
|
4762
4839
|
* Harness for interacting with `tn-chip-input` in tests. Reads the committed
|