@truenas/ui-components 0.1.23 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,8 @@
1
1
  import * as i0 from '@angular/core';
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, afterNextRender, PLATFORM_ID, DestroyRef } from '@angular/core';
2
+ import { inject, ElementRef, input, output, viewChild, signal, computed, effect, forwardRef, Component, ChangeDetectionStrategy, Injectable, ViewEncapsulation, Directive, contentChildren, ViewContainerRef, contentChild, ChangeDetectorRef, HostListener, TemplateRef, IterableDiffers, Pipe, model, afterNextRender, PLATFORM_ID, DestroyRef } from '@angular/core';
3
+ import * as i1$3 from '@angular/forms';
4
+ import { NG_VALUE_ACCESSOR, FormsModule, NgControl } from '@angular/forms';
5
+ import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
3
6
  import * as i1$1 from '@angular/common';
4
7
  import { CommonModule, DOCUMENT, isPlatformBrowser } from '@angular/common';
5
8
  import { mdiCheckCircle, mdiAlertCircle, mdiAlert, mdiInformation, mdiDotsVertical, mdiClose, mdiFolderOpen, mdiLock, mdiLoading, mdiFolderPlus, mdiFolderNetwork, mdiHarddisk, mdiDatabase, mdiFile, mdiFolder } from '@mdi/js';
@@ -7,11 +10,8 @@ import * as i1 from '@angular/platform-browser';
7
10
  import { DomSanitizer } from '@angular/platform-browser';
8
11
  import { HttpClient } from '@angular/common/http';
9
12
  import { firstValueFrom, BehaviorSubject, merge, Subject } from 'rxjs';
10
- import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
11
13
  import * as i1$4 from '@angular/cdk/a11y';
12
14
  import { FocusMonitor, A11yModule } from '@angular/cdk/a11y';
13
- import * as i1$3 from '@angular/forms';
14
- import { FormsModule, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
15
15
  import { Overlay, OverlayModule, OverlayPositionBuilder } from '@angular/cdk/overlay';
16
16
  import { TemplatePortal, PortalModule, ComponentPortal } from '@angular/cdk/portal';
17
17
  import { CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
@@ -26,6 +26,435 @@ import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
26
26
  import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
27
27
  import { ScrollingModule } from '@angular/cdk/scrolling';
28
28
 
29
+ let nextId = 0;
30
+ class TnAutocompleteComponent {
31
+ elementRef = inject(ElementRef);
32
+ /** Unique instance ID for ARIA linkage */
33
+ uid = `tn-autocomplete-${nextId++}`;
34
+ /** All available options */
35
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
36
+ /** Transform a value to its display string */
37
+ displayWith = input((v) => String(v), ...(ngDevMode ? [{ debugName: "displayWith" }] : []));
38
+ /** Placeholder text for the input */
39
+ placeholder = input('Type to search...', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
40
+ /** Whether the input is disabled */
41
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
42
+ /** Require the user to select from the dropdown — reverts on blur if no match */
43
+ requireSelection = input(false, ...(ngDevMode ? [{ debugName: "requireSelection" }] : []));
44
+ /** Custom filter function. Defaults to case-insensitive includes on displayWith text */
45
+ filterFn = input(undefined, ...(ngDevMode ? [{ debugName: "filterFn" }] : []));
46
+ /** Text shown when no options match the search */
47
+ noResultsText = input('No results found', ...(ngDevMode ? [{ debugName: "noResultsText" }] : []));
48
+ /** Maximum number of options to render */
49
+ maxResults = input(100, ...(ngDevMode ? [{ debugName: "maxResults" }] : []));
50
+ /** Test ID attribute */
51
+ testId = input('', ...(ngDevMode ? [{ debugName: "testId" }] : []));
52
+ /** Emits when an option is selected */
53
+ optionSelected = output();
54
+ /** Reference to the input element */
55
+ inputEl = viewChild('inputEl', ...(ngDevMode ? [{ debugName: "inputEl" }] : []));
56
+ /** Current search term typed by the user */
57
+ searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
58
+ /** Whether the dropdown is open */
59
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
60
+ /** Index of the currently highlighted option for keyboard nav */
61
+ highlightedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "highlightedIndex" }] : []));
62
+ /** The currently selected value */
63
+ selectedValue = signal(null, ...(ngDevMode ? [{ debugName: "selectedValue" }] : []));
64
+ /** CVA disabled state from the form */
65
+ formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "formDisabled" }] : []));
66
+ /** Combined disabled state */
67
+ isDisabled = computed(() => this.disabled() || this.formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
68
+ /** Filtered and capped options */
69
+ filteredOptions = computed(() => {
70
+ const term = this.searchTerm();
71
+ const all = this.options();
72
+ const customFilter = this.filterFn();
73
+ const display = this.displayWith();
74
+ const max = this.maxResults();
75
+ if (!term) {
76
+ return all.slice(0, max);
77
+ }
78
+ const lowerTerm = term.toLowerCase();
79
+ const filtered = customFilter
80
+ ? all.filter((opt) => customFilter(opt, term))
81
+ : all.filter((opt) => display(opt).toLowerCase().includes(lowerTerm));
82
+ return filtered.slice(0, max);
83
+ }, ...(ngDevMode ? [{ debugName: "filteredOptions" }] : []));
84
+ /** Whether there are any results to show */
85
+ hasResults = computed(() => this.filteredOptions().length > 0, ...(ngDevMode ? [{ debugName: "hasResults" }] : []));
86
+ onChange = (_value) => { };
87
+ onTouched = () => { };
88
+ constructor() {
89
+ // Click-outside detection
90
+ effect(() => {
91
+ if (this.isOpen()) {
92
+ const listener = (event) => {
93
+ if (!this.elementRef.nativeElement.contains(event.target)) {
94
+ this.close();
95
+ }
96
+ };
97
+ setTimeout(() => {
98
+ document.addEventListener('click', listener);
99
+ }, 0);
100
+ return () => {
101
+ document.removeEventListener('click', listener);
102
+ };
103
+ }
104
+ return undefined;
105
+ });
106
+ }
107
+ // ── ControlValueAccessor ──
108
+ writeValue(value) {
109
+ this.selectedValue.set(value);
110
+ if (value !== null && value !== undefined) {
111
+ this.searchTerm.set(this.displayWith()(value));
112
+ }
113
+ else {
114
+ this.searchTerm.set('');
115
+ }
116
+ }
117
+ registerOnChange(fn) {
118
+ this.onChange = fn;
119
+ }
120
+ registerOnTouched(fn) {
121
+ this.onTouched = fn;
122
+ }
123
+ setDisabledState(isDisabled) {
124
+ this.formDisabled.set(isDisabled);
125
+ }
126
+ // ── Event handlers ──
127
+ onInput(event) {
128
+ const value = event.target.value;
129
+ this.searchTerm.set(value);
130
+ this.highlightedIndex.set(-1);
131
+ if (!this.isOpen()) {
132
+ this.isOpen.set(true);
133
+ }
134
+ }
135
+ onFocus() {
136
+ if (!this.isDisabled()) {
137
+ this.isOpen.set(true);
138
+ }
139
+ }
140
+ onBlur() {
141
+ if (this.requireSelection()) {
142
+ const term = this.searchTerm();
143
+ const display = this.displayWith();
144
+ const match = this.options().find((opt) => display(opt).toLowerCase() === term.toLowerCase());
145
+ if (match) {
146
+ this.selectOption(match);
147
+ }
148
+ else {
149
+ // Revert to last valid selection or clear
150
+ const current = this.selectedValue();
151
+ if (current !== null && current !== undefined) {
152
+ this.searchTerm.set(display(current));
153
+ }
154
+ else {
155
+ this.searchTerm.set('');
156
+ this.onChange(null);
157
+ }
158
+ }
159
+ }
160
+ this.close();
161
+ this.onTouched();
162
+ }
163
+ onOptionClick(option) {
164
+ this.selectOption(option);
165
+ }
166
+ onKeydown(event) {
167
+ const options = this.filteredOptions();
168
+ switch (event.key) {
169
+ case 'ArrowDown':
170
+ event.preventDefault();
171
+ if (!this.isOpen()) {
172
+ this.isOpen.set(true);
173
+ }
174
+ else {
175
+ this.highlightedIndex.update((i) => i < options.length - 1 ? i + 1 : 0);
176
+ this.scrollToHighlighted();
177
+ }
178
+ break;
179
+ case 'ArrowUp':
180
+ event.preventDefault();
181
+ if (this.isOpen()) {
182
+ this.highlightedIndex.update((i) => i > 0 ? i - 1 : options.length - 1);
183
+ this.scrollToHighlighted();
184
+ }
185
+ break;
186
+ case 'Enter':
187
+ event.preventDefault();
188
+ const idx = this.highlightedIndex();
189
+ if (this.isOpen() && idx >= 0 && idx < options.length) {
190
+ this.selectOption(options[idx]);
191
+ }
192
+ break;
193
+ case 'Escape':
194
+ event.preventDefault();
195
+ this.close();
196
+ break;
197
+ }
198
+ }
199
+ // ── Internal ──
200
+ selectOption(option) {
201
+ this.selectedValue.set(option);
202
+ this.searchTerm.set(this.displayWith()(option));
203
+ this.onChange(option);
204
+ this.optionSelected.emit(option);
205
+ this.close();
206
+ }
207
+ close() {
208
+ this.isOpen.set(false);
209
+ this.highlightedIndex.set(-1);
210
+ }
211
+ scrollToHighlighted() {
212
+ const idx = this.highlightedIndex();
213
+ const dropdown = this.elementRef.nativeElement.querySelector('.tn-autocomplete__dropdown');
214
+ const options = dropdown?.querySelectorAll('.tn-autocomplete__option');
215
+ if (options?.[idx]?.scrollIntoView) {
216
+ options[idx].scrollIntoView({ block: 'nearest' });
217
+ }
218
+ }
219
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
220
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnAutocompleteComponent, isStandalone: true, selector: "tn-autocomplete", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requireSelection: { classPropertyName: "requireSelection", publicName: "requireSelection", isSignal: true, isRequired: false, transformFunction: null }, filterFn: { classPropertyName: "filterFn", publicName: "filterFn", isSignal: true, isRequired: false, transformFunction: null }, noResultsText: { classPropertyName: "noResultsText", publicName: "noResultsText", isSignal: true, isRequired: false, transformFunction: null }, maxResults: { classPropertyName: "maxResults", publicName: "maxResults", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { optionSelected: "optionSelected" }, providers: [
221
+ {
222
+ provide: NG_VALUE_ACCESSOR,
223
+ useExisting: forwardRef(() => TnAutocompleteComponent),
224
+ multi: true,
225
+ },
226
+ ], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-autocomplete\" [attr.data-testid]=\"testId()\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n\n @if (isOpen()) {\n <div\n class=\"tn-autocomplete__dropdown\"\n role=\"listbox\"\n [attr.id]=\"uid + '-dropdown'\">\n\n @if (hasResults()) {\n @for (option of filteredOptions(); track $index) {\n <div\n class=\"tn-autocomplete__option\"\n role=\"option\"\n tabindex=\"-1\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ displayWith()(option) }}\n </div>\n }\n } @else {\n <div class=\"tn-autocomplete__no-results\">\n {{ noResultsText() }}\n </div>\n }\n </div>\n }\n</div>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:.875rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{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-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover{background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.tn-autocomplete__no-results{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}@media(prefers-reduced-motion:reduce){.tn-autocomplete__input,.tn-autocomplete__option{transition:none}}\n"] });
227
+ }
228
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, decorators: [{
229
+ type: Component,
230
+ args: [{ selector: 'tn-autocomplete', standalone: true, imports: [], providers: [
231
+ {
232
+ provide: NG_VALUE_ACCESSOR,
233
+ useExisting: forwardRef(() => TnAutocompleteComponent),
234
+ multi: true,
235
+ },
236
+ ], template: "<div class=\"tn-autocomplete\" [attr.data-testid]=\"testId()\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n\n @if (isOpen()) {\n <div\n class=\"tn-autocomplete__dropdown\"\n role=\"listbox\"\n [attr.id]=\"uid + '-dropdown'\">\n\n @if (hasResults()) {\n @for (option of filteredOptions(); track $index) {\n <div\n class=\"tn-autocomplete__option\"\n role=\"option\"\n tabindex=\"-1\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ displayWith()(option) }}\n </div>\n }\n } @else {\n <div class=\"tn-autocomplete__no-results\">\n {{ noResultsText() }}\n </div>\n }\n </div>\n }\n</div>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:.875rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{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-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover{background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.tn-autocomplete__no-results{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}@media(prefers-reduced-motion:reduce){.tn-autocomplete__input,.tn-autocomplete__option{transition:none}}\n"] }]
237
+ }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], displayWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayWith", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requireSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "requireSelection", required: false }] }], filterFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterFn", required: false }] }], noResultsText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noResultsText", required: false }] }], maxResults: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxResults", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], optionSelected: [{ type: i0.Output, args: ["optionSelected"] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }] } });
238
+
239
+ /**
240
+ * Harness for interacting with tn-autocomplete in tests.
241
+ * Provides methods for querying state, typing search text, and selecting options.
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * // Find and select an option
246
+ * const ac = await loader.getHarness(TnAutocompleteHarness);
247
+ * await ac.selectOption('United States');
248
+ *
249
+ * // Type to filter, then read options
250
+ * await ac.setInputValue('Can');
251
+ * const options = await ac.getOptions();
252
+ * expect(options).toEqual(['Canada']);
253
+ *
254
+ * // Filter by placeholder
255
+ * const countryAc = await loader.getHarness(
256
+ * TnAutocompleteHarness.with({ placeholder: 'Search countries...' })
257
+ * );
258
+ * ```
259
+ */
260
+ class TnAutocompleteHarness extends ComponentHarness {
261
+ /**
262
+ * The selector for the host element of a `TnAutocompleteComponent` instance.
263
+ */
264
+ static hostSelector = 'tn-autocomplete';
265
+ _input = this.locatorFor('.tn-autocomplete__input');
266
+ /**
267
+ * Gets a `HarnessPredicate` that can be used to search for an autocomplete
268
+ * with specific attributes.
269
+ *
270
+ * @param options Options for filtering which autocomplete instances are considered a match.
271
+ * @returns A `HarnessPredicate` configured with the given options.
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * // Find by exact placeholder
276
+ * const ac = await loader.getHarness(
277
+ * TnAutocompleteHarness.with({ placeholder: 'Search countries...' })
278
+ * );
279
+ * ```
280
+ */
281
+ static with(options = {}) {
282
+ return new HarnessPredicate(TnAutocompleteHarness, options)
283
+ .addOption('placeholder', options.placeholder, async (harness, placeholder) => {
284
+ return (await harness.getPlaceholder()) === placeholder;
285
+ });
286
+ }
287
+ /**
288
+ * Gets the current value of the input field.
289
+ *
290
+ * @returns Promise resolving to the input value.
291
+ *
292
+ * @example
293
+ * ```typescript
294
+ * const ac = await loader.getHarness(TnAutocompleteHarness);
295
+ * await ac.selectOption('Canada');
296
+ * expect(await ac.getInputValue()).toBe('Canada');
297
+ * ```
298
+ */
299
+ async getInputValue() {
300
+ const input = await this._input();
301
+ return (await input.getProperty('value')) ?? '';
302
+ }
303
+ /**
304
+ * Sets the value of the input by clearing and typing.
305
+ * This triggers filtering of the dropdown options.
306
+ *
307
+ * @param value The text to type into the input.
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * const ac = await loader.getHarness(TnAutocompleteHarness);
312
+ * await ac.setInputValue('Can');
313
+ * const options = await ac.getOptions();
314
+ * expect(options).toEqual(['Canada']);
315
+ * ```
316
+ */
317
+ async setInputValue(value) {
318
+ const input = await this._input();
319
+ await input.clear();
320
+ await input.sendKeys(value);
321
+ }
322
+ /**
323
+ * Gets the placeholder text of the input.
324
+ *
325
+ * @returns Promise resolving to the placeholder string, or null.
326
+ *
327
+ * @example
328
+ * ```typescript
329
+ * const ac = await loader.getHarness(TnAutocompleteHarness);
330
+ * expect(await ac.getPlaceholder()).toBe('Type to search...');
331
+ * ```
332
+ */
333
+ async getPlaceholder() {
334
+ const input = await this._input();
335
+ return input.getAttribute('placeholder');
336
+ }
337
+ /**
338
+ * Checks whether the dropdown is currently open.
339
+ *
340
+ * @returns Promise resolving to true if the dropdown is visible.
341
+ *
342
+ * @example
343
+ * ```typescript
344
+ * const ac = await loader.getHarness(TnAutocompleteHarness);
345
+ * expect(await ac.isOpen()).toBe(false);
346
+ * await ac.focus();
347
+ * expect(await ac.isOpen()).toBe(true);
348
+ * ```
349
+ */
350
+ async isOpen() {
351
+ const dropdown = await this.locatorForOptional('.tn-autocomplete__dropdown')();
352
+ return dropdown !== null;
353
+ }
354
+ /**
355
+ * Checks whether the autocomplete input is disabled.
356
+ *
357
+ * @returns Promise resolving to true if the input is disabled.
358
+ *
359
+ * @example
360
+ * ```typescript
361
+ * const ac = await loader.getHarness(TnAutocompleteHarness);
362
+ * expect(await ac.isDisabled()).toBe(false);
363
+ * ```
364
+ */
365
+ async isDisabled() {
366
+ const input = await this._input();
367
+ return (await input.getProperty('disabled')) ?? false;
368
+ }
369
+ /**
370
+ * Gets the labels of all visible options in the dropdown.
371
+ * Opens the dropdown via focus if not already open.
372
+ *
373
+ * @returns Promise resolving to an array of option label strings.
374
+ *
375
+ * @example
376
+ * ```typescript
377
+ * const ac = await loader.getHarness(TnAutocompleteHarness);
378
+ * await ac.setInputValue('C');
379
+ * const options = await ac.getOptions();
380
+ * expect(options).toContain('Canada');
381
+ * ```
382
+ */
383
+ async getOptions() {
384
+ const options = await this.locatorForAll('.tn-autocomplete__option')();
385
+ const labels = [];
386
+ for (const option of options) {
387
+ labels.push((await option.text()).trim());
388
+ }
389
+ return labels;
390
+ }
391
+ /**
392
+ * Selects an option by its label text. Opens the dropdown via focus if needed.
393
+ *
394
+ * @param filter The text to match against option labels. Supports string or RegExp.
395
+ * @returns Promise that resolves when the option has been selected.
396
+ * @throws Error if no matching option is found.
397
+ *
398
+ * @example
399
+ * ```typescript
400
+ * const ac = await loader.getHarness(TnAutocompleteHarness);
401
+ *
402
+ * // Select by exact text
403
+ * await ac.selectOption('Canada');
404
+ *
405
+ * // Select by regex
406
+ * await ac.selectOption(/united/i);
407
+ * ```
408
+ */
409
+ async selectOption(filter) {
410
+ const input = await this._input();
411
+ await input.focus();
412
+ const options = await this.locatorForAll('.tn-autocomplete__option')();
413
+ for (const option of options) {
414
+ const text = (await option.text()).trim();
415
+ const matches = filter instanceof RegExp ? filter.test(text) : text === filter;
416
+ if (matches) {
417
+ await option.click();
418
+ return;
419
+ }
420
+ }
421
+ throw new Error(`Could not find autocomplete option matching "${filter}"`);
422
+ }
423
+ /**
424
+ * Focuses the autocomplete input, which opens the dropdown.
425
+ *
426
+ * @returns Promise that resolves when the input is focused.
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * const ac = await loader.getHarness(TnAutocompleteHarness);
431
+ * await ac.focus();
432
+ * expect(await ac.isOpen()).toBe(true);
433
+ * ```
434
+ */
435
+ async focus() {
436
+ const input = await this._input();
437
+ return input.focus();
438
+ }
439
+ /**
440
+ * Blurs the autocomplete input, which closes the dropdown.
441
+ *
442
+ * @returns Promise that resolves when the input is blurred.
443
+ *
444
+ * @example
445
+ * ```typescript
446
+ * const ac = await loader.getHarness(TnAutocompleteHarness);
447
+ * await ac.focus();
448
+ * await ac.blur();
449
+ * expect(await ac.isOpen()).toBe(false);
450
+ * ```
451
+ */
452
+ async blur() {
453
+ const input = await this._input();
454
+ return input.blur();
455
+ }
456
+ }
457
+
29
458
  var DiskType;
30
459
  (function (DiskType) {
31
460
  DiskType["Hdd"] = "HDD";
@@ -9989,5 +10418,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
9989
10418
  * Generated bundle index. Do not edit.
9990
10419
  */
9991
10420
 
9992
- export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, 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, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTabsComponent, TnTabsHarness, TnTheme, TnThemeService, TnTimeInputComponent, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
10421
+ export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCellDefDirective, TnCheckboxComponent, TnChipComponent, TnConfirmDialogComponent, TnDateInputComponent, TnDateRangeInputComponent, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTabsComponent, TnTabsHarness, TnTheme, TnThemeService, TnTimeInputComponent, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
9993
10422
  //# sourceMappingURL=truenas-ui-components.mjs.map