@truenas/ui-components 0.1.65 → 0.1.67
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,7 @@
|
|
|
1
|
+
import { Overlay, OverlayPositionBuilder, OverlayModule } from '@angular/cdk/overlay';
|
|
2
|
+
import { TemplatePortal, ComponentPortal, PortalModule } from '@angular/cdk/portal';
|
|
1
3
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, Renderer2, ElementRef, input, effect, Directive, output, viewChild,
|
|
4
|
+
import { InjectionToken, inject, Renderer2, ElementRef, input, effect, Directive, ViewContainerRef, output, viewChild, computed, signal, forwardRef, Component, model, afterNextRender, ChangeDetectionStrategy, Injectable, isDevMode, ViewEncapsulation, contentChildren, HostListener, contentChild, ChangeDetectorRef, TemplateRef, DestroyRef, isSignal, untracked, IterableDiffers, Pipe, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
|
|
3
5
|
import * as i2 from '@angular/forms';
|
|
4
6
|
import { NG_VALUE_ACCESSOR, FormsModule, NgControl } from '@angular/forms';
|
|
5
7
|
import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
|
|
@@ -13,8 +15,6 @@ import { DomSanitizer } from '@angular/platform-browser';
|
|
|
13
15
|
import { HttpClient } from '@angular/common/http';
|
|
14
16
|
import { firstValueFrom, BehaviorSubject, merge, Subject, take } from 'rxjs';
|
|
15
17
|
import { RouterLink } from '@angular/router';
|
|
16
|
-
import { Overlay, OverlayPositionBuilder, OverlayModule } from '@angular/cdk/overlay';
|
|
17
|
-
import { ComponentPortal, TemplatePortal, PortalModule } from '@angular/cdk/portal';
|
|
18
18
|
import { CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
|
|
19
19
|
import { trigger, state, transition, style, animate } from '@angular/animations';
|
|
20
20
|
import { SPACE, ENTER, END, HOME, DOWN_ARROW, UP_ARROW, RIGHT_ARROW, LEFT_ARROW } from '@angular/cdk/keycodes';
|
|
@@ -96,6 +96,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
96
96
|
let nextId$1 = 0;
|
|
97
97
|
class TnAutocompleteComponent {
|
|
98
98
|
elementRef = inject(ElementRef);
|
|
99
|
+
overlay = inject(Overlay);
|
|
100
|
+
viewContainerRef = inject(ViewContainerRef);
|
|
99
101
|
/** Unique instance ID for ARIA linkage */
|
|
100
102
|
uid = `tn-autocomplete-${nextId$1++}`;
|
|
101
103
|
/** All available options */
|
|
@@ -112,14 +114,34 @@ class TnAutocompleteComponent {
|
|
|
112
114
|
filterFn = input(undefined, ...(ngDevMode ? [{ debugName: "filterFn" }] : []));
|
|
113
115
|
/** Text shown when no options match the search */
|
|
114
116
|
noResultsText = input('No results found', ...(ngDevMode ? [{ debugName: "noResultsText" }] : []));
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Maximum number of options to render.
|
|
119
|
+
*
|
|
120
|
+
* Defaults to `Infinity` so browsing the open dropdown without a search term
|
|
121
|
+
* never silently truncates a long list. Pass an explicit cap if you want to
|
|
122
|
+
* limit how many filtered results are rendered at once.
|
|
123
|
+
*/
|
|
124
|
+
maxResults = input(Infinity, ...(ngDevMode ? [{ debugName: "maxResults" }] : []));
|
|
125
|
+
/**
|
|
126
|
+
* Max height of the dropdown panel before it scrolls. Accepts a number
|
|
127
|
+
* (interpreted as `px`) or any CSS length string (e.g. `'320px'`,
|
|
128
|
+
* `'min(320px, 40vh)'`). Surfaced as the `--tn-autocomplete-panel-max-height`
|
|
129
|
+
* custom property so it can also be themed in CSS.
|
|
130
|
+
*/
|
|
131
|
+
panelMaxHeight = input('320px', ...(ngDevMode ? [{ debugName: "panelMaxHeight" }] : []));
|
|
117
132
|
/** Test ID attribute */
|
|
118
133
|
testId = input('', ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
119
134
|
/** Emits when an option is selected */
|
|
120
135
|
optionSelected = output();
|
|
121
136
|
/** Reference to the input element */
|
|
122
137
|
inputEl = viewChild('inputEl', ...(ngDevMode ? [{ debugName: "inputEl" }] : []));
|
|
138
|
+
/** Template for the dropdown panel, portaled into a CDK overlay on open. */
|
|
139
|
+
dropdownTemplate = viewChild.required('dropdownTemplate');
|
|
140
|
+
/** Normalized panel max-height as a CSS length string. */
|
|
141
|
+
panelMaxHeightValue = computed(() => {
|
|
142
|
+
const value = this.panelMaxHeight();
|
|
143
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
144
|
+
}, ...(ngDevMode ? [{ debugName: "panelMaxHeightValue" }] : []));
|
|
123
145
|
/** Current search term typed by the user */
|
|
124
146
|
searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
|
|
125
147
|
/** Whether the dropdown is open */
|
|
@@ -152,24 +174,14 @@ class TnAutocompleteComponent {
|
|
|
152
174
|
hasResults = computed(() => this.filteredOptions().length > 0, ...(ngDevMode ? [{ debugName: "hasResults" }] : []));
|
|
153
175
|
onChange = (_value) => { };
|
|
154
176
|
onTouched = () => { };
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
};
|
|
164
|
-
setTimeout(() => {
|
|
165
|
-
document.addEventListener('click', listener);
|
|
166
|
-
}, 0);
|
|
167
|
-
return () => {
|
|
168
|
-
document.removeEventListener('click', listener);
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
return undefined;
|
|
172
|
-
});
|
|
177
|
+
/** Live overlay holding the dropdown panel, or undefined when closed. */
|
|
178
|
+
overlayRef;
|
|
179
|
+
/** Subscriptions tied to the current overlay; torn down on close. */
|
|
180
|
+
overlaySubs = [];
|
|
181
|
+
ngOnDestroy() {
|
|
182
|
+
// If the component is destroyed while the dropdown is open (e.g. router
|
|
183
|
+
// navigates away), close() never runs — clean up the overlay directly.
|
|
184
|
+
this.detachOverlay();
|
|
173
185
|
}
|
|
174
186
|
// ── ControlValueAccessor ──
|
|
175
187
|
writeValue(value) {
|
|
@@ -196,12 +208,17 @@ class TnAutocompleteComponent {
|
|
|
196
208
|
this.searchTerm.set(value);
|
|
197
209
|
this.highlightedIndex.set(-1);
|
|
198
210
|
if (!this.isOpen()) {
|
|
199
|
-
this.
|
|
211
|
+
this.open();
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
// Filtering changes the panel's height; nudge CDK to re-evaluate the
|
|
215
|
+
// anchored position so it stays attached to the input.
|
|
216
|
+
this.overlayRef?.updatePosition();
|
|
200
217
|
}
|
|
201
218
|
}
|
|
202
219
|
onFocus() {
|
|
203
220
|
if (!this.isDisabled()) {
|
|
204
|
-
this.
|
|
221
|
+
this.open();
|
|
205
222
|
}
|
|
206
223
|
}
|
|
207
224
|
onBlur() {
|
|
@@ -236,7 +253,7 @@ class TnAutocompleteComponent {
|
|
|
236
253
|
case 'ArrowDown':
|
|
237
254
|
event.preventDefault();
|
|
238
255
|
if (!this.isOpen()) {
|
|
239
|
-
this.
|
|
256
|
+
this.open();
|
|
240
257
|
}
|
|
241
258
|
else {
|
|
242
259
|
this.highlightedIndex.update((i) => i < options.length - 1 ? i + 1 : 0);
|
|
@@ -271,26 +288,81 @@ class TnAutocompleteComponent {
|
|
|
271
288
|
this.optionSelected.emit(option);
|
|
272
289
|
this.close();
|
|
273
290
|
}
|
|
291
|
+
open() {
|
|
292
|
+
if (this.isDisabled() || this.isOpen()) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
this.isOpen.set(true);
|
|
296
|
+
this.attachOverlay();
|
|
297
|
+
}
|
|
274
298
|
close() {
|
|
275
299
|
this.isOpen.set(false);
|
|
276
300
|
this.highlightedIndex.set(-1);
|
|
301
|
+
this.detachOverlay();
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Attach the dropdown panel as a CDK overlay anchored to the input.
|
|
305
|
+
*
|
|
306
|
+
* Why an overlay (vs. an inline absolutely-positioned panel): the panel is
|
|
307
|
+
* appended to the overlay container on `document.body`, so it can never be
|
|
308
|
+
* clipped by an ancestor's `overflow: hidden`/`auto` (e.g. a surrounding
|
|
309
|
+
* card). CDK also flips the panel above the input near the viewport edge and
|
|
310
|
+
* matches its width to the input. Mirrors TnSelectComponent's approach.
|
|
311
|
+
*/
|
|
312
|
+
attachOverlay() {
|
|
313
|
+
const anchor = this.inputEl()?.nativeElement;
|
|
314
|
+
if (!anchor) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const positionStrategy = this.overlay
|
|
318
|
+
.position()
|
|
319
|
+
.flexibleConnectedTo(anchor)
|
|
320
|
+
.withPositions([
|
|
321
|
+
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
|
|
322
|
+
{ originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
|
|
323
|
+
]);
|
|
324
|
+
this.overlayRef = this.overlay.create({
|
|
325
|
+
positionStrategy,
|
|
326
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
327
|
+
hasBackdrop: false,
|
|
328
|
+
width: anchor.offsetWidth,
|
|
329
|
+
});
|
|
330
|
+
this.overlayRef.attach(new TemplatePortal(this.dropdownTemplate(), this.viewContainerRef));
|
|
331
|
+
// Non-intercepting click-outside: the pointer event still reaches whatever
|
|
332
|
+
// the user clicked; we just notice and close. Ignore targets inside the
|
|
333
|
+
// host (the input itself) so we don't fight the focus/typing handlers.
|
|
334
|
+
this.overlaySubs.push(this.overlayRef.outsidePointerEvents().subscribe((event) => {
|
|
335
|
+
const target = event.target;
|
|
336
|
+
if (target && this.elementRef.nativeElement.contains(target)) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
this.close();
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
detachOverlay() {
|
|
343
|
+
this.overlaySubs.forEach((sub) => sub.unsubscribe());
|
|
344
|
+
this.overlaySubs = [];
|
|
345
|
+
this.overlayRef?.dispose();
|
|
346
|
+
this.overlayRef = undefined;
|
|
277
347
|
}
|
|
278
348
|
scrollToHighlighted() {
|
|
279
349
|
const idx = this.highlightedIndex();
|
|
280
|
-
|
|
281
|
-
|
|
350
|
+
// The panel lives in the CDK overlay (outside the host), so scope the query
|
|
351
|
+
// to the overlay element rather than the host element.
|
|
352
|
+
const overlayEl = this.overlayRef?.overlayElement;
|
|
353
|
+
const options = overlayEl?.querySelectorAll('.tn-autocomplete__option');
|
|
282
354
|
if (options?.[idx]?.scrollIntoView) {
|
|
283
355
|
options[idx].scrollIntoView({ block: 'nearest' });
|
|
284
356
|
}
|
|
285
357
|
}
|
|
286
358
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
287
|
-
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: [
|
|
359
|
+
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 }, panelMaxHeight: { classPropertyName: "panelMaxHeight", publicName: "panelMaxHeight", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { optionSelected: "optionSelected" }, providers: [
|
|
288
360
|
{
|
|
289
361
|
provide: NG_VALUE_ACCESSOR,
|
|
290
362
|
useExisting: forwardRef(() => TnAutocompleteComponent),
|
|
291
363
|
multi: true,
|
|
292
364
|
},
|
|
293
|
-
], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-autocomplete\" [tnTestId]=\"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
|
|
365
|
+
], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-autocomplete\" [tnTestId]=\"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-owns]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. Attach/detach (driven by open()/close()) controls\n visibility; the input keeps DOM focus throughout. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-autocomplete__dropdown\"\n role=\"listbox\"\n [style.--tn-autocomplete-panel-max-height]=\"panelMaxHeightValue()\"\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</ng-template>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:1rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-autocomplete-panel-max-height, 320px);overflow-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover{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"], dependencies: [{ kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId"] }] });
|
|
294
366
|
}
|
|
295
367
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, decorators: [{
|
|
296
368
|
type: Component,
|
|
@@ -300,8 +372,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
300
372
|
useExisting: forwardRef(() => TnAutocompleteComponent),
|
|
301
373
|
multi: true,
|
|
302
374
|
},
|
|
303
|
-
], template: "<div class=\"tn-autocomplete\" [tnTestId]=\"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
|
|
304
|
-
}],
|
|
375
|
+
], template: "<div class=\"tn-autocomplete\" [tnTestId]=\"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-owns]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. Attach/detach (driven by open()/close()) controls\n visibility; the input keeps DOM focus throughout. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-autocomplete__dropdown\"\n role=\"listbox\"\n [style.--tn-autocomplete-panel-max-height]=\"panelMaxHeightValue()\"\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</ng-template>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:1rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-autocomplete-panel-max-height, 320px);overflow-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover{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"] }]
|
|
376
|
+
}], 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 }] }], panelMaxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelMaxHeight", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], optionSelected: [{ type: i0.Output, args: ["optionSelected"] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
|
|
305
377
|
|
|
306
378
|
/**
|
|
307
379
|
* Harness for interacting with tn-autocomplete in tests.
|
|
@@ -415,8 +487,11 @@ class TnAutocompleteHarness extends ComponentHarness {
|
|
|
415
487
|
* ```
|
|
416
488
|
*/
|
|
417
489
|
async isOpen() {
|
|
418
|
-
|
|
419
|
-
|
|
490
|
+
// The dropdown panel is rendered in a CDK overlay (outside the host), so we
|
|
491
|
+
// read the open state from the `.open` class the input carries while the
|
|
492
|
+
// panel is attached rather than looking for the panel in the host subtree.
|
|
493
|
+
const input = await this._input();
|
|
494
|
+
return input.hasClass('open');
|
|
420
495
|
}
|
|
421
496
|
/**
|
|
422
497
|
* Checks whether the autocomplete input is disabled.
|
|
@@ -448,7 +523,9 @@ class TnAutocompleteHarness extends ComponentHarness {
|
|
|
448
523
|
* ```
|
|
449
524
|
*/
|
|
450
525
|
async getOptions() {
|
|
451
|
-
|
|
526
|
+
// Options render inside a CDK overlay (outside the host element), so search
|
|
527
|
+
// the document root rather than the harness-local subtree.
|
|
528
|
+
const options = await this.documentRootLocatorFactory().locatorForAll('.tn-autocomplete__option')();
|
|
452
529
|
const labels = [];
|
|
453
530
|
for (const option of options) {
|
|
454
531
|
labels.push((await option.text()).trim());
|
|
@@ -476,7 +553,9 @@ class TnAutocompleteHarness extends ComponentHarness {
|
|
|
476
553
|
async selectOption(filter) {
|
|
477
554
|
const input = await this._input();
|
|
478
555
|
await input.focus();
|
|
479
|
-
|
|
556
|
+
// Options render inside a CDK overlay (outside the host element), so search
|
|
557
|
+
// the document root rather than the harness-local subtree.
|
|
558
|
+
const options = await this.documentRootLocatorFactory().locatorForAll('.tn-autocomplete__option')();
|
|
480
559
|
for (const option of options) {
|
|
481
560
|
const text = (await option.text()).trim();
|
|
482
561
|
const matches = filter instanceof RegExp ? filter.test(text) : text === filter;
|
|
@@ -5614,6 +5693,10 @@ class TnFormFieldComponent {
|
|
|
5614
5693
|
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
5615
5694
|
testId = input('', ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
5616
5695
|
subscriptSizing = input('dynamic', ...(ngDevMode ? [{ debugName: "subscriptSizing" }] : []));
|
|
5696
|
+
/** Optional tooltip shown via a help icon next to the label. */
|
|
5697
|
+
tooltip = input('', ...(ngDevMode ? [{ debugName: "tooltip" }] : []));
|
|
5698
|
+
/** Placement of the tooltip relative to its help icon. */
|
|
5699
|
+
tooltipPosition = input('above', ...(ngDevMode ? [{ debugName: "tooltipPosition" }] : []));
|
|
5617
5700
|
control = contentChild(NgControl, ...(ngDevMode ? [{ debugName: "control" }] : []));
|
|
5618
5701
|
hasError = signal(false, ...(ngDevMode ? [{ debugName: "hasError" }] : []));
|
|
5619
5702
|
errorMessage = signal('', ...(ngDevMode ? [{ debugName: "errorMessage" }] : []));
|
|
@@ -5681,12 +5764,12 @@ class TnFormFieldComponent {
|
|
|
5681
5764
|
return this.subscriptSizing() === 'fixed' || this.showError() || this.showHint();
|
|
5682
5765
|
}, ...(ngDevMode ? [{ debugName: "showSubscript" }] : []));
|
|
5683
5766
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFormFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5684
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFormFieldComponent, isStandalone: true, selector: "tn-form-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, subscriptSizing: { classPropertyName: "subscriptSizing", publicName: "subscriptSizing", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "control", first: true, predicate: NgControl, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-form-field\" [tnTestId]=\"testId()\">\n <!-- Label -->\n @if (label()) {\n <label class=\"tn-form-field-label\" [class.required]=\"required()\">\n
|
|
5767
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFormFieldComponent, isStandalone: true, selector: "tn-form-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, subscriptSizing: { classPropertyName: "subscriptSizing", publicName: "subscriptSizing", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tooltipPosition: { classPropertyName: "tooltipPosition", publicName: "tooltipPosition", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "control", first: true, predicate: NgControl, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-form-field\" [tnTestId]=\"testId()\">\n <!-- Label -->\n @if (label() || tooltip()) {\n <div class=\"tn-form-field-label-row\">\n @if (label()) {\n <label class=\"tn-form-field-label\" [class.required]=\"required()\">\n {{ label() }}\n @if (required()) {\n <span class=\"required-asterisk\" aria-label=\"required\">*</span>\n }\n </label>\n }\n @if (tooltip()) {\n <button\n type=\"button\"\n class=\"tn-form-field-tooltip\"\n [attr.aria-label]=\"tooltip()\"\n [tnTooltip]=\"tooltip()\"\n [tnTooltipPosition]=\"tooltipPosition()\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" />\n </button>\n }\n </div>\n }\n\n <!-- Form Control Content -->\n <div class=\"tn-form-field-wrapper\">\n <ng-content />\n </div>\n\n <!-- Hint or Error Message -->\n @if (showSubscript()) {\n <div class=\"tn-form-field-subscript\" [class.tn-form-field-subscript-dynamic]=\"subscriptSizing() === 'dynamic'\">\n @if (showError()) {\n <div\n class=\"tn-form-field-error\"\n role=\"alert\"\n aria-live=\"polite\">\n {{ errorMessage() }}\n </div>\n }\n @if (showHint()) {\n <div class=\"tn-form-field-hint\">\n {{ hint() }}\n </div>\n }\n </div>\n }\n</div>\n", styles: [".tn-form-field{display:block;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-form-field-label-row{display:flex;align-items:center;gap:.375rem;margin-bottom:.5rem}.tn-form-field-label{display:block;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-form-field-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-form-field-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-form-field-tooltip:hover,.tn-form-field-tooltip:focus-visible{color:var(--tn-primary, #007bff)}.tn-form-field-tooltip:focus-visible{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;border-radius:50%}.tn-form-field-wrapper{position:relative;width:100%;overflow:visible}.tn-form-field-wrapper :ng-deep .tn-select-container,.tn-form-field-wrapper :ng-deep .tn-input-container{margin-bottom:0}.tn-form-field-wrapper :ng-deep .tn-select-label,.tn-form-field-wrapper :ng-deep .tn-input-label{display:none}.tn-form-field-wrapper :ng-deep .tn-select-error,.tn-form-field-wrapper :ng-deep .tn-input-error{display:none}.tn-form-field-wrapper :ng-deep .tn-select-dropdown{z-index:1000}.tn-form-field-subscript{min-height:1.25rem;margin-top:.25rem;font-size:.75rem;line-height:1.4}.tn-form-field-subscript-dynamic{min-height:0}.tn-form-field-error{color:var(--tn-error, #dc3545);margin:0}.tn-form-field-hint{color:var(--tn-fg2, #6c757d);margin:0}.tn-form-field-wrapper:has(:focus-visible) .tn-form-field-label{color:var(--tn-primary, #007bff)}.tn-form-field-wrapper:has(.error) .tn-form-field-label{color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-form-field-label{transition:none}}@media(prefers-contrast:high){.tn-form-field-label,.tn-form-field-error{font-weight:600}}\n"], dependencies: [{ kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "fullSize", "customSize"] }, { kind: "directive", type: TnTooltipDirective, selector: "[tnTooltip]", inputs: ["tnTooltip", "tnTooltipPosition", "tnTooltipDisabled", "tnTooltipShowDelay", "tnTooltipHideDelay", "tnTooltipClass"] }] });
|
|
5685
5768
|
}
|
|
5686
5769
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFormFieldComponent, decorators: [{
|
|
5687
5770
|
type: Component,
|
|
5688
|
-
args: [{ selector: 'tn-form-field', standalone: true, imports: [TnTestIdDirective], template: "<div class=\"tn-form-field\" [tnTestId]=\"testId()\">\n <!-- Label -->\n @if (label()) {\n <label class=\"tn-form-field-label\" [class.required]=\"required()\">\n
|
|
5689
|
-
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], subscriptSizing: [{ type: i0.Input, args: [{ isSignal: true, alias: "subscriptSizing", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NgControl), { isSignal: true }] }] } });
|
|
5771
|
+
args: [{ selector: 'tn-form-field', standalone: true, imports: [TnTestIdDirective, TnIconComponent, TnTooltipDirective], template: "<div class=\"tn-form-field\" [tnTestId]=\"testId()\">\n <!-- Label -->\n @if (label() || tooltip()) {\n <div class=\"tn-form-field-label-row\">\n @if (label()) {\n <label class=\"tn-form-field-label\" [class.required]=\"required()\">\n {{ label() }}\n @if (required()) {\n <span class=\"required-asterisk\" aria-label=\"required\">*</span>\n }\n </label>\n }\n @if (tooltip()) {\n <button\n type=\"button\"\n class=\"tn-form-field-tooltip\"\n [attr.aria-label]=\"tooltip()\"\n [tnTooltip]=\"tooltip()\"\n [tnTooltipPosition]=\"tooltipPosition()\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" />\n </button>\n }\n </div>\n }\n\n <!-- Form Control Content -->\n <div class=\"tn-form-field-wrapper\">\n <ng-content />\n </div>\n\n <!-- Hint or Error Message -->\n @if (showSubscript()) {\n <div class=\"tn-form-field-subscript\" [class.tn-form-field-subscript-dynamic]=\"subscriptSizing() === 'dynamic'\">\n @if (showError()) {\n <div\n class=\"tn-form-field-error\"\n role=\"alert\"\n aria-live=\"polite\">\n {{ errorMessage() }}\n </div>\n }\n @if (showHint()) {\n <div class=\"tn-form-field-hint\">\n {{ hint() }}\n </div>\n }\n </div>\n }\n</div>\n", styles: [".tn-form-field{display:block;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-form-field-label-row{display:flex;align-items:center;gap:.375rem;margin-bottom:.5rem}.tn-form-field-label{display:block;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-form-field-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-form-field-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-form-field-tooltip:hover,.tn-form-field-tooltip:focus-visible{color:var(--tn-primary, #007bff)}.tn-form-field-tooltip:focus-visible{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;border-radius:50%}.tn-form-field-wrapper{position:relative;width:100%;overflow:visible}.tn-form-field-wrapper :ng-deep .tn-select-container,.tn-form-field-wrapper :ng-deep .tn-input-container{margin-bottom:0}.tn-form-field-wrapper :ng-deep .tn-select-label,.tn-form-field-wrapper :ng-deep .tn-input-label{display:none}.tn-form-field-wrapper :ng-deep .tn-select-error,.tn-form-field-wrapper :ng-deep .tn-input-error{display:none}.tn-form-field-wrapper :ng-deep .tn-select-dropdown{z-index:1000}.tn-form-field-subscript{min-height:1.25rem;margin-top:.25rem;font-size:.75rem;line-height:1.4}.tn-form-field-subscript-dynamic{min-height:0}.tn-form-field-error{color:var(--tn-error, #dc3545);margin:0}.tn-form-field-hint{color:var(--tn-fg2, #6c757d);margin:0}.tn-form-field-wrapper:has(:focus-visible) .tn-form-field-label{color:var(--tn-primary, #007bff)}.tn-form-field-wrapper:has(.error) .tn-form-field-label{color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-form-field-label{transition:none}}@media(prefers-contrast:high){.tn-form-field-label,.tn-form-field-error{font-weight:600}}\n"] }]
|
|
5772
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], subscriptSizing: [{ type: i0.Input, args: [{ isSignal: true, alias: "subscriptSizing", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], tooltipPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipPosition", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NgControl), { isSignal: true }] }] } });
|
|
5690
5773
|
|
|
5691
5774
|
/**
|
|
5692
5775
|
* Harness for interacting with `tn-form-field` in tests.
|
|
@@ -5719,6 +5802,7 @@ class TnFormFieldHarness extends ComponentHarness {
|
|
|
5719
5802
|
_label = this.locatorForOptional('.tn-form-field-label');
|
|
5720
5803
|
_error = this.locatorForOptional('.tn-form-field-error');
|
|
5721
5804
|
_hint = this.locatorForOptional('.tn-form-field-hint');
|
|
5805
|
+
_tooltip = this.locatorForOptional('.tn-form-field-tooltip');
|
|
5722
5806
|
/**
|
|
5723
5807
|
* Gets a `HarnessPredicate` that can be used to search for a form field
|
|
5724
5808
|
* with specific attributes.
|
|
@@ -5810,6 +5894,36 @@ class TnFormFieldHarness extends ComponentHarness {
|
|
|
5810
5894
|
const hint = await this._hint();
|
|
5811
5895
|
return hint ? (await hint.text()).trim() : null;
|
|
5812
5896
|
}
|
|
5897
|
+
/**
|
|
5898
|
+
* Checks whether the form field has a tooltip help icon.
|
|
5899
|
+
*
|
|
5900
|
+
* @returns Promise resolving to true if the tooltip trigger is present.
|
|
5901
|
+
*
|
|
5902
|
+
* @example
|
|
5903
|
+
* ```typescript
|
|
5904
|
+
* const field = await loader.getHarness(TnFormFieldHarness.with({ label: 'Purpose' }));
|
|
5905
|
+
* expect(await field.hasTooltip()).toBe(true);
|
|
5906
|
+
* ```
|
|
5907
|
+
*/
|
|
5908
|
+
async hasTooltip() {
|
|
5909
|
+
const tooltip = await this._tooltip();
|
|
5910
|
+
return tooltip !== null;
|
|
5911
|
+
}
|
|
5912
|
+
/**
|
|
5913
|
+
* Gets the tooltip message (read from the trigger's accessible label).
|
|
5914
|
+
*
|
|
5915
|
+
* @returns Promise resolving to the tooltip text, or null if no tooltip.
|
|
5916
|
+
*
|
|
5917
|
+
* @example
|
|
5918
|
+
* ```typescript
|
|
5919
|
+
* const field = await loader.getHarness(TnFormFieldHarness.with({ label: 'Purpose' }));
|
|
5920
|
+
* expect(await field.getTooltip()).toBe('What this share is used for');
|
|
5921
|
+
* ```
|
|
5922
|
+
*/
|
|
5923
|
+
async getTooltip() {
|
|
5924
|
+
const tooltip = await this._tooltip();
|
|
5925
|
+
return tooltip ? tooltip.getAttribute('aria-label') : null;
|
|
5926
|
+
}
|
|
5813
5927
|
/**
|
|
5814
5928
|
* Checks whether the form field is marked as required.
|
|
5815
5929
|
*
|