@truenas/ui-components 0.3.6 → 0.3.7
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.
|
@@ -4375,20 +4375,37 @@ let nextId = 0;
|
|
|
4375
4375
|
* committed to a chip on Enter (or a configurable separator key); Backspace on
|
|
4376
4376
|
* an empty field removes the last chip.
|
|
4377
4377
|
*
|
|
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.
|
|
4378
|
+
* It is a `ControlValueAccessor` over `T[]` (defaulting to `string[]`), so it
|
|
4379
|
+
* drops into a reactive or template-driven form (`[formControl]`, `[(ngModel)]`)
|
|
4380
|
+
* and slots into a `tn-form-field` as a real projected control — the field's
|
|
4381
|
+
* required/error inference reads this control directly.
|
|
4382
4382
|
*
|
|
4383
|
-
*
|
|
4384
|
-
*
|
|
4385
|
-
*
|
|
4386
|
-
*
|
|
4383
|
+
* **String mode (default).** Pass `[suggestions]` (a `string[]`) for typeahead;
|
|
4384
|
+
* the typed/picked string is the value. Set `allowCustomValue=false` to restrict
|
|
4385
|
+
* commits to the suggestion list.
|
|
4386
|
+
*
|
|
4387
|
+
* **Value mode.** Pass `[options]` (`{ label, value }[]`) to display labels while
|
|
4388
|
+
* committing values — the model becomes `T[]`. A written value resolves to its
|
|
4389
|
+
* option's label (falling back to `String(value)` until the option is
|
|
4390
|
+
* available). Provide `[compareWith]` when the values are objects. Committing a
|
|
4391
|
+
* typed string matches an option by label (case-insensitive); free text that
|
|
4392
|
+
* matches no option is only accepted when `allowCustomValue` is `true` (which is
|
|
4393
|
+
* only sound for string-valued inputs).
|
|
4394
|
+
*
|
|
4395
|
+
* Either source can be driven asynchronously by listening to `(searchChange)`
|
|
4396
|
+
* and updating `[suggestions]`/`[options]` as results arrive. The dropdown is
|
|
4397
|
+
* portaled through a CDK overlay so it escapes any ancestor `overflow: hidden`.
|
|
4387
4398
|
*
|
|
4388
4399
|
* @example
|
|
4389
4400
|
* ```html
|
|
4401
|
+
* <!-- string mode -->
|
|
4390
4402
|
* <tn-form-field label="Tags">
|
|
4391
|
-
* <tn-chip-input [formControl]="tags"
|
|
4403
|
+
* <tn-chip-input [formControl]="tags" [suggestions]="tagSuggestions" />
|
|
4404
|
+
* </tn-form-field>
|
|
4405
|
+
*
|
|
4406
|
+
* <!-- value mode: shows names, commits ids -->
|
|
4407
|
+
* <tn-form-field label="Groups">
|
|
4408
|
+
* <tn-chip-input [formControl]="groupIds" [options]="groupOptions" [allowCustomValue]="false" />
|
|
4392
4409
|
* </tn-form-field>
|
|
4393
4410
|
* ```
|
|
4394
4411
|
*/
|
|
@@ -4410,30 +4427,42 @@ class TnChipInputComponent {
|
|
|
4410
4427
|
/** Commit a pending (non-empty) text value as a chip when the field loses focus. */
|
|
4411
4428
|
addOnBlur = input(false, ...(ngDevMode ? [{ debugName: "addOnBlur" }] : []));
|
|
4412
4429
|
/**
|
|
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
|
-
*
|
|
4430
|
+
* Whether free text not matching any option/suggestion may be committed.
|
|
4431
|
+
* Defaults to `true` — any typed value becomes a chip. Set `false` to restrict
|
|
4432
|
+
* the field to its list (a "pick from the list" control): a commit only
|
|
4433
|
+
* succeeds when the text matches an option/suggestion label (case-insensitive,
|
|
4434
|
+
* committing the canonical entry); unmatched text is discarded. Mirrors
|
|
4435
|
+
* `tn-autocomplete`'s `allowCustomValue`. In value mode, leave this `false` —
|
|
4436
|
+
* fabricating a typed string as a non-string value is unsound.
|
|
4420
4437
|
*/
|
|
4421
4438
|
allowCustomValue = input(true, ...(ngDevMode ? [{ debugName: "allowCustomValue" }] : []));
|
|
4422
4439
|
/**
|
|
4423
4440
|
* 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.
|
|
4441
|
+
* Duplicate detection uses `compareWith` (or identity); string-mode matching
|
|
4442
|
+
* is exact (case-sensitive), so `Angular` and `angular` are distinct — only
|
|
4443
|
+
* the *filtering* of suggestions is case-insensitive.
|
|
4427
4444
|
*/
|
|
4428
4445
|
allowDuplicates = input(false, ...(ngDevMode ? [{ debugName: "allowDuplicates" }] : []));
|
|
4429
4446
|
/** Hard cap on the number of chips; `undefined` means no limit. */
|
|
4430
4447
|
maxChips = input(undefined, ...(ngDevMode ? [{ debugName: "maxChips" }] : []));
|
|
4431
4448
|
/**
|
|
4432
|
-
*
|
|
4433
|
-
* match the typed text and are not already selected.
|
|
4434
|
-
* update this in response to `(searchChange)`.
|
|
4449
|
+
* String-mode suggestion list. When non-empty, a dropdown offers entries that
|
|
4450
|
+
* match the typed text and are not already selected. Ignored when `options`
|
|
4451
|
+
* is provided. For async sources, update this in response to `(searchChange)`.
|
|
4435
4452
|
*/
|
|
4436
4453
|
suggestions = input([], ...(ngDevMode ? [{ debugName: "suggestions" }] : []));
|
|
4454
|
+
/**
|
|
4455
|
+
* Value-mode option list (`{ label, value }`). When non-empty, chips display
|
|
4456
|
+
* the resolved `label` while the form model holds `value`s. Takes precedence
|
|
4457
|
+
* over `suggestions`. For async sources, update in response to `(searchChange)`.
|
|
4458
|
+
*/
|
|
4459
|
+
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
4460
|
+
/**
|
|
4461
|
+
* Comparator for value equality — used for de-duplication, display resolution
|
|
4462
|
+
* and the selected-set. Defaults to identity (`===`), correct for primitives;
|
|
4463
|
+
* provide this when values are objects (e.g. `(a, b) => a?.id === b?.id`).
|
|
4464
|
+
*/
|
|
4465
|
+
compareWith = input(undefined, ...(ngDevMode ? [{ debugName: "compareWith" }] : []));
|
|
4437
4466
|
/** Accessible name for the text field. Leave unset inside a `tn-form-field`. */
|
|
4438
4467
|
ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
4439
4468
|
/**
|
|
@@ -4474,15 +4503,25 @@ class TnChipInputComponent {
|
|
|
4474
4503
|
const max = this.maxChips();
|
|
4475
4504
|
return max === undefined || this.values().length < max;
|
|
4476
4505
|
}, ...(ngDevMode ? [{ debugName: "canAddMore" }] : []));
|
|
4477
|
-
/**
|
|
4506
|
+
/**
|
|
4507
|
+
* Unified option list. Value-mode `options` win; otherwise string-mode
|
|
4508
|
+
* `suggestions` are lifted into `{ label: s, value: s }`.
|
|
4509
|
+
*/
|
|
4510
|
+
optionList = computed(() => {
|
|
4511
|
+
const opts = this.options();
|
|
4512
|
+
if (opts.length) {
|
|
4513
|
+
return opts;
|
|
4514
|
+
}
|
|
4515
|
+
return this.suggestions().map((suggestion) => ({ label: suggestion, value: suggestion }));
|
|
4516
|
+
}, ...(ngDevMode ? [{ debugName: "optionList" }] : []));
|
|
4517
|
+
/** Options matching the typed text and not already selected. */
|
|
4478
4518
|
filteredSuggestions = computed(() => {
|
|
4479
|
-
const selected = new Set(this.values());
|
|
4480
4519
|
const term = this.inputValue().trim().toLowerCase();
|
|
4481
|
-
return this.
|
|
4482
|
-
if (
|
|
4520
|
+
return this.optionList().filter((option) => {
|
|
4521
|
+
if (this.valuesIncludes(option.value)) {
|
|
4483
4522
|
return false;
|
|
4484
4523
|
}
|
|
4485
|
-
return term === '' ||
|
|
4524
|
+
return term === '' || option.label.toLowerCase().includes(term);
|
|
4486
4525
|
});
|
|
4487
4526
|
}, ...(ngDevMode ? [{ debugName: "filteredSuggestions" }] : []));
|
|
4488
4527
|
onChange = () => { };
|
|
@@ -4492,10 +4531,10 @@ class TnChipInputComponent {
|
|
|
4492
4531
|
constructor() {
|
|
4493
4532
|
// Async suggestions: when the user types, onInput runs syncDropdown()
|
|
4494
4533
|
// 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
|
|
4534
|
+
// tick later via [suggestions]/[options]. Re-open the panel once fresh
|
|
4535
|
+
// matches arrive while the field is focused and actively searching. This
|
|
4536
|
+
// only ever opens (never closes), so it doesn't fight Escape, blur, or the
|
|
4537
|
+
// post-commit close — those stay shut until the option set next changes.
|
|
4499
4538
|
effect(() => {
|
|
4500
4539
|
const hasMatches = this.filteredSuggestions().length > 0;
|
|
4501
4540
|
untracked(() => {
|
|
@@ -4552,7 +4591,7 @@ class TnChipInputComponent {
|
|
|
4552
4591
|
onBlur() {
|
|
4553
4592
|
this.focused.set(false);
|
|
4554
4593
|
if (this.addOnBlur()) {
|
|
4555
|
-
this.
|
|
4594
|
+
this.commitText(this.inputValue());
|
|
4556
4595
|
}
|
|
4557
4596
|
this.close();
|
|
4558
4597
|
this.onTouched();
|
|
@@ -4594,10 +4633,10 @@ class TnChipInputComponent {
|
|
|
4594
4633
|
event.preventDefault();
|
|
4595
4634
|
const idx = this.highlightedIndex();
|
|
4596
4635
|
if (this.isOpen() && idx >= 0 && idx < suggestions.length) {
|
|
4597
|
-
this.
|
|
4636
|
+
this.commitValue(suggestions[idx].value);
|
|
4598
4637
|
}
|
|
4599
4638
|
else {
|
|
4600
|
-
this.
|
|
4639
|
+
this.commitText(this.inputValue());
|
|
4601
4640
|
}
|
|
4602
4641
|
return;
|
|
4603
4642
|
}
|
|
@@ -4608,8 +4647,8 @@ class TnChipInputComponent {
|
|
|
4608
4647
|
this.removeChip(this.values().length - 1);
|
|
4609
4648
|
}
|
|
4610
4649
|
}
|
|
4611
|
-
onSuggestionClick(
|
|
4612
|
-
this.
|
|
4650
|
+
onSuggestionClick(option) {
|
|
4651
|
+
this.commitValue(option.value);
|
|
4613
4652
|
this.inputEl().nativeElement.focus();
|
|
4614
4653
|
}
|
|
4615
4654
|
/** Prevents the option `mousedown` from blurring the input before the click lands. */
|
|
@@ -4621,7 +4660,7 @@ class TnChipInputComponent {
|
|
|
4621
4660
|
return;
|
|
4622
4661
|
}
|
|
4623
4662
|
const removed = this.values()[index];
|
|
4624
|
-
if (
|
|
4663
|
+
if (index < 0 || index >= this.values().length) {
|
|
4625
4664
|
return;
|
|
4626
4665
|
}
|
|
4627
4666
|
this.values.update((values) => values.filter((_, i) => i !== index));
|
|
@@ -4634,6 +4673,11 @@ class TnChipInputComponent {
|
|
|
4634
4673
|
this.inputEl().nativeElement.focus();
|
|
4635
4674
|
this.syncDropdown();
|
|
4636
4675
|
}
|
|
4676
|
+
/** The label shown on a chip for a committed value. */
|
|
4677
|
+
displayLabel(value) {
|
|
4678
|
+
const match = this.optionList().find((option) => this.valueMatches(option.value, value));
|
|
4679
|
+
return match ? match.label : String(value);
|
|
4680
|
+
}
|
|
4637
4681
|
/** Scopes a per-chip test id beneath the component's base. */
|
|
4638
4682
|
chipTestId(value) {
|
|
4639
4683
|
const base = this.testId();
|
|
@@ -4646,22 +4690,29 @@ class TnChipInputComponent {
|
|
|
4646
4690
|
isCommitKey(event) {
|
|
4647
4691
|
return event.key === 'Enter' || this.separatorKeys().includes(event.key);
|
|
4648
4692
|
}
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4693
|
+
/** Commits typed text: resolve it to an option's value, else accept as custom. */
|
|
4694
|
+
commitText(raw) {
|
|
4695
|
+
const text = (raw ?? '').trim();
|
|
4696
|
+
if (!text) {
|
|
4652
4697
|
return;
|
|
4653
4698
|
}
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
if (!match) {
|
|
4659
|
-
this.clearInput();
|
|
4660
|
-
return;
|
|
4661
|
-
}
|
|
4662
|
-
value = match;
|
|
4699
|
+
const match = this.optionList().find((option) => option.label.toLowerCase() === text.toLowerCase());
|
|
4700
|
+
if (match) {
|
|
4701
|
+
this.commitValue(match.value);
|
|
4702
|
+
return;
|
|
4663
4703
|
}
|
|
4664
|
-
if (
|
|
4704
|
+
if (this.allowCustomValue()) {
|
|
4705
|
+
this.commitValue(text);
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
this.clearInput();
|
|
4709
|
+
}
|
|
4710
|
+
/** Commits a resolved value, honouring duplicate and cap rules. */
|
|
4711
|
+
commitValue(value) {
|
|
4712
|
+
if (this.isDisabled() || !this.canAddMore()) {
|
|
4713
|
+
return;
|
|
4714
|
+
}
|
|
4715
|
+
if (!this.allowDuplicates() && this.valuesIncludes(value)) {
|
|
4665
4716
|
this.clearInput();
|
|
4666
4717
|
return;
|
|
4667
4718
|
}
|
|
@@ -4671,6 +4722,13 @@ class TnChipInputComponent {
|
|
|
4671
4722
|
this.chipAdded.emit(value);
|
|
4672
4723
|
this.clearInput();
|
|
4673
4724
|
}
|
|
4725
|
+
valuesIncludes(value) {
|
|
4726
|
+
return this.values().some((existing) => this.valueMatches(existing, value));
|
|
4727
|
+
}
|
|
4728
|
+
valueMatches(a, b) {
|
|
4729
|
+
const comparator = this.compareWith();
|
|
4730
|
+
return comparator ? comparator(a, b) : a === b;
|
|
4731
|
+
}
|
|
4674
4732
|
clearInput() {
|
|
4675
4733
|
this.inputValue.set('');
|
|
4676
4734
|
this.inputEl().nativeElement.value = '';
|
|
@@ -4679,7 +4737,7 @@ class TnChipInputComponent {
|
|
|
4679
4737
|
/**
|
|
4680
4738
|
* Opens the dropdown when there is something to show, closes it otherwise.
|
|
4681
4739
|
* Stays closed once the chip cap is reached — suggesting rows that
|
|
4682
|
-
* `
|
|
4740
|
+
* `commitValue()` would reject is misleading.
|
|
4683
4741
|
*/
|
|
4684
4742
|
syncDropdown() {
|
|
4685
4743
|
if (this.filteredSuggestions().length > 0 && this.canAddMore() && !this.isDisabled()) {
|
|
@@ -4739,13 +4797,13 @@ class TnChipInputComponent {
|
|
|
4739
4797
|
this.overlayRef = undefined;
|
|
4740
4798
|
}
|
|
4741
4799
|
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: [
|
|
4800
|
+
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
4801
|
{
|
|
4744
4802
|
provide: NG_VALUE_ACCESSOR,
|
|
4745
4803
|
useExisting: forwardRef(() => TnChipInputComponent),
|
|
4746
4804
|
multi: true,
|
|
4747
4805
|
},
|
|
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
|
|
4806
|
+
], 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
4807
|
}
|
|
4750
4808
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnChipInputComponent, decorators: [{
|
|
4751
4809
|
type: Component,
|
|
@@ -4755,8 +4813,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
4755
4813
|
useExisting: forwardRef(() => TnChipInputComponent),
|
|
4756
4814
|
multi: true,
|
|
4757
4815
|
},
|
|
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 }] }] } });
|
|
4816
|
+
], 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"] }]
|
|
4817
|
+
}], 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
4818
|
|
|
4761
4819
|
/**
|
|
4762
4820
|
* Harness for interacting with `tn-chip-input` in tests. Reads the committed
|