@truenas/ui-components 0.1.64 → 0.1.66
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.
|
@@ -93,11 +93,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
93
93
|
}]
|
|
94
94
|
}], ctorParameters: () => [], propDecorators: { testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTestId", required: false }] }] } });
|
|
95
95
|
|
|
96
|
-
let nextId = 0;
|
|
96
|
+
let nextId$1 = 0;
|
|
97
97
|
class TnAutocompleteComponent {
|
|
98
98
|
elementRef = inject(ElementRef);
|
|
99
99
|
/** Unique instance ID for ARIA linkage */
|
|
100
|
-
uid = `tn-autocomplete-${nextId++}`;
|
|
100
|
+
uid = `tn-autocomplete-${nextId$1++}`;
|
|
101
101
|
/** All available options */
|
|
102
102
|
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
103
103
|
/** Transform a value to its display string */
|
|
@@ -2261,10 +2261,15 @@ class TnIconButtonHarness extends ComponentHarness {
|
|
|
2261
2261
|
var InputType;
|
|
2262
2262
|
(function (InputType) {
|
|
2263
2263
|
InputType["Email"] = "email";
|
|
2264
|
+
InputType["Number"] = "number";
|
|
2264
2265
|
InputType["Password"] = "password";
|
|
2265
2266
|
InputType["PlainText"] = "text";
|
|
2266
2267
|
})(InputType || (InputType = {}));
|
|
2267
2268
|
|
|
2269
|
+
// Module-level counter for deterministic, unique instance ids (matches the
|
|
2270
|
+
// tn-autocomplete convention). Deterministic ids are SSR/hydration-safe and
|
|
2271
|
+
// stable across snapshots, unlike a Math.random() seed.
|
|
2272
|
+
let nextId = 0;
|
|
2268
2273
|
class TnInputComponent {
|
|
2269
2274
|
inputEl = viewChild.required('inputEl');
|
|
2270
2275
|
inputType = input(InputType.PlainText, ...(ngDevMode ? [{ debugName: "inputType" }] : []));
|
|
@@ -2273,6 +2278,25 @@ class TnInputComponent {
|
|
|
2273
2278
|
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
2274
2279
|
multiline = input(false, ...(ngDevMode ? [{ debugName: "multiline" }] : []));
|
|
2275
2280
|
rows = input(3, ...(ngDevMode ? [{ debugName: "rows" }] : []));
|
|
2281
|
+
/**
|
|
2282
|
+
* Accessible name for the control. Rendered as `aria-label` on the input/textarea.
|
|
2283
|
+
*
|
|
2284
|
+
* Leave unset when the input is wrapped in a `tn-form-field` (or otherwise has a
|
|
2285
|
+
* visible `<label>`); set it for standalone usage so the control isn't unnamed in
|
|
2286
|
+
* the a11y tree.
|
|
2287
|
+
*/
|
|
2288
|
+
ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
2289
|
+
/**
|
|
2290
|
+
* Integer/decimal switch — only meaningful when `inputType` is `InputType.Number`.
|
|
2291
|
+
*
|
|
2292
|
+
* - **`true` (default) → decimal mode**: accepts a single `.` and emits via
|
|
2293
|
+
* `parseFloat`, so a `Number` field accepts `"3.5"` out of the box.
|
|
2294
|
+
* - **`false` → integer mode**: strips `.` and emits via `parseInt`.
|
|
2295
|
+
*
|
|
2296
|
+
* Range enforcement is intentionally left to the consumer's form validators
|
|
2297
|
+
* (e.g. `Validators.min`/`max`), which work because the control emits real numbers.
|
|
2298
|
+
*/
|
|
2299
|
+
allowDecimals = input(true, ...(ngDevMode ? [{ debugName: "allowDecimals" }] : []));
|
|
2276
2300
|
// Icon inputs
|
|
2277
2301
|
prefixIcon = input(undefined, ...(ngDevMode ? [{ debugName: "prefixIcon" }] : []));
|
|
2278
2302
|
prefixIconLibrary = input(undefined, ...(ngDevMode ? [{ debugName: "prefixIconLibrary" }] : []));
|
|
@@ -2282,20 +2306,60 @@ class TnInputComponent {
|
|
|
2282
2306
|
onSuffixAction = output();
|
|
2283
2307
|
hasPrefixIcon = computed(() => !!this.prefixIcon(), ...(ngDevMode ? [{ debugName: "hasPrefixIcon" }] : []));
|
|
2284
2308
|
hasSuffixIcon = computed(() => !!this.suffixIcon(), ...(ngDevMode ? [{ debugName: "hasSuffixIcon" }] : []));
|
|
2285
|
-
|
|
2309
|
+
// Numeric mode state
|
|
2310
|
+
isNumeric = computed(() => this.inputType() === InputType.Number, ...(ngDevMode ? [{ debugName: "isNumeric" }] : []));
|
|
2311
|
+
/** True when the field only accepts whole numbers (i.e. `allowDecimals` is false). */
|
|
2312
|
+
integerOnly = computed(() => !this.allowDecimals(), ...(ngDevMode ? [{ debugName: "integerOnly" }] : []));
|
|
2313
|
+
/** The `type` attribute actually rendered. Number mode uses a text input + inputmode to avoid the native type="number" footguns. */
|
|
2314
|
+
resolvedType = computed(() => (this.isNumeric() ? InputType.PlainText : this.inputType()), ...(ngDevMode ? [{ debugName: "resolvedType" }] : []));
|
|
2315
|
+
/** `inputmode` hint: numeric keypad for integers, decimal keypad otherwise. Null (omitted) when not in number mode. */
|
|
2316
|
+
numericInputMode = computed(() => {
|
|
2317
|
+
if (!this.isNumeric()) {
|
|
2318
|
+
return null;
|
|
2319
|
+
}
|
|
2320
|
+
return this.integerOnly() ? 'numeric' : 'decimal';
|
|
2321
|
+
}, ...(ngDevMode ? [{ debugName: "numericInputMode" }] : []));
|
|
2322
|
+
/** Number mode is always single-line; it wins over `multiline` if both are set. */
|
|
2323
|
+
showTextarea = computed(() => this.multiline() && !this.isNumeric(), ...(ngDevMode ? [{ debugName: "showTextarea" }] : []));
|
|
2324
|
+
// Unique per instance: a hard-coded id produced duplicate ids when multiple
|
|
2325
|
+
// tn-inputs share a page (invalid HTML, and it breaks any future label `for=`,
|
|
2326
|
+
// aria-describedby, or getElementById targeting this input).
|
|
2327
|
+
id = `tn-input-${nextId++}`;
|
|
2328
|
+
// Protected: the display string is owned by the component and driven through the
|
|
2329
|
+
// CVA flow (writeValue / onValueChange). External writes would bypass that flow.
|
|
2286
2330
|
value = '';
|
|
2287
2331
|
// CVA disabled state management
|
|
2288
2332
|
formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "formDisabled" }] : []));
|
|
2289
2333
|
isDisabled = computed(() => this.disabled() || this.formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
2290
|
-
onChange = (
|
|
2334
|
+
onChange = () => { };
|
|
2291
2335
|
onTouched = () => { };
|
|
2292
2336
|
focusMonitor = inject(FocusMonitor);
|
|
2337
|
+
// The element handed to FocusMonitor, captured so ngOnDestroy can stop monitoring
|
|
2338
|
+
// exactly what it started — without re-reading the required viewChild on a
|
|
2339
|
+
// component that may be destroyed before its view ever initialized.
|
|
2340
|
+
monitoredEl;
|
|
2293
2341
|
ngAfterViewInit() {
|
|
2294
|
-
this.
|
|
2342
|
+
this.monitoredEl = this.inputEl();
|
|
2343
|
+
this.focusMonitor.monitor(this.monitoredEl);
|
|
2344
|
+
}
|
|
2345
|
+
ngOnDestroy() {
|
|
2346
|
+
// Stop the FocusMonitor we started in ngAfterViewInit; otherwise it keeps a
|
|
2347
|
+
// DOM observer alive on a detached element.
|
|
2348
|
+
if (this.monitoredEl) {
|
|
2349
|
+
this.focusMonitor.stopMonitoring(this.monitoredEl);
|
|
2350
|
+
}
|
|
2295
2351
|
}
|
|
2296
2352
|
// ControlValueAccessor implementation
|
|
2353
|
+
// Accepts undefined as well as the declared types: Angular may hand writeValue an
|
|
2354
|
+
// undefined model at init, and it maps to the empty display like null.
|
|
2297
2355
|
writeValue(value) {
|
|
2298
|
-
|
|
2356
|
+
// Display the model verbatim via String(); do NOT sanitize here. Sanitizing a
|
|
2357
|
+
// canonical number string would corrupt fractions in integer mode (3.5 -> "35"),
|
|
2358
|
+
// silently diverging the display from the form model.
|
|
2359
|
+
//
|
|
2360
|
+
// Scientific notation is out of scope: users can't type 'e' (it's stripped), and
|
|
2361
|
+
// exponential-range values aren't meaningful for the fields this control targets.
|
|
2362
|
+
this.value = value === null || value === undefined ? '' : String(value);
|
|
2299
2363
|
}
|
|
2300
2364
|
registerOnChange(fn) {
|
|
2301
2365
|
this.onChange = fn;
|
|
@@ -2306,23 +2370,66 @@ class TnInputComponent {
|
|
|
2306
2370
|
setDisabledState(isDisabled) {
|
|
2307
2371
|
this.formDisabled.set(isDisabled);
|
|
2308
2372
|
}
|
|
2309
|
-
// Component methods
|
|
2373
|
+
// Component methods (template-bound; protected — not part of the public API)
|
|
2310
2374
|
onValueChange(event) {
|
|
2311
2375
|
const target = event.target;
|
|
2376
|
+
if (this.isNumeric()) {
|
|
2377
|
+
const el = target;
|
|
2378
|
+
const sanitized = this.sanitizeNumeric(el.value);
|
|
2379
|
+
// Reflect the cleaned string back into the DOM when we stripped something
|
|
2380
|
+
// (e.g. the user typed "e", "+" or a second "."), keeping the field honest.
|
|
2381
|
+
if (sanitized !== el.value) {
|
|
2382
|
+
// Preserve the caret: its new position is however many characters survive
|
|
2383
|
+
// sanitization up to the old caret (sanitizeNumeric is a left-to-right filter).
|
|
2384
|
+
const caret = this.sanitizeNumeric(el.value.slice(0, el.selectionStart ?? el.value.length)).length;
|
|
2385
|
+
el.value = sanitized;
|
|
2386
|
+
el.setSelectionRange(caret, caret);
|
|
2387
|
+
}
|
|
2388
|
+
this.value = sanitized;
|
|
2389
|
+
this.onChange(this.parseNumeric(sanitized));
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2312
2392
|
this.value = target.value;
|
|
2313
2393
|
this.onChange(this.value);
|
|
2314
2394
|
}
|
|
2315
2395
|
onBlur() {
|
|
2316
2396
|
this.onTouched();
|
|
2317
2397
|
}
|
|
2398
|
+
/** Strips any character that can't appear in the current numeric mode (single leading '-', single '.' for decimals). */
|
|
2399
|
+
sanitizeNumeric(raw) {
|
|
2400
|
+
const allowDecimal = !this.integerOnly();
|
|
2401
|
+
let result = '';
|
|
2402
|
+
let hasDot = false;
|
|
2403
|
+
for (const char of raw) {
|
|
2404
|
+
if (char >= '0' && char <= '9') {
|
|
2405
|
+
result += char;
|
|
2406
|
+
}
|
|
2407
|
+
else if (char === '-' && result.length === 0) {
|
|
2408
|
+
result += char;
|
|
2409
|
+
}
|
|
2410
|
+
else if (char === '.' && allowDecimal && !hasDot) {
|
|
2411
|
+
hasDot = true;
|
|
2412
|
+
result += char;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
return result;
|
|
2416
|
+
}
|
|
2417
|
+
/** Parses a sanitized string to a number, mapping empty/partial input to null (never 0). */
|
|
2418
|
+
parseNumeric(value) {
|
|
2419
|
+
if (value === '' || value === '-' || value === '.' || value === '-.') {
|
|
2420
|
+
return null;
|
|
2421
|
+
}
|
|
2422
|
+
const parsed = this.integerOnly() ? parseInt(value, 10) : parseFloat(value);
|
|
2423
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
2424
|
+
}
|
|
2318
2425
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2319
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnInputComponent, isStandalone: true, selector: "tn-input", inputs: { inputType: { classPropertyName: "inputType", publicName: "inputType", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, multiline: { classPropertyName: "multiline", publicName: "multiline", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, prefixIcon: { classPropertyName: "prefixIcon", publicName: "prefixIcon", isSignal: true, isRequired: false, transformFunction: null }, prefixIconLibrary: { classPropertyName: "prefixIconLibrary", publicName: "prefixIconLibrary", isSignal: true, isRequired: false, transformFunction: null }, suffixIcon: { classPropertyName: "suffixIcon", publicName: "suffixIcon", isSignal: true, isRequired: false, transformFunction: null }, suffixIconLibrary: { classPropertyName: "suffixIconLibrary", publicName: "suffixIconLibrary", isSignal: true, isRequired: false, transformFunction: null }, suffixIconAriaLabel: { classPropertyName: "suffixIconAriaLabel", publicName: "suffixIconAriaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSuffixAction: "onSuffixAction" }, providers: [
|
|
2426
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnInputComponent, isStandalone: true, selector: "tn-input", inputs: { inputType: { classPropertyName: "inputType", publicName: "inputType", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, multiline: { classPropertyName: "multiline", publicName: "multiline", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, allowDecimals: { classPropertyName: "allowDecimals", publicName: "allowDecimals", isSignal: true, isRequired: false, transformFunction: null }, prefixIcon: { classPropertyName: "prefixIcon", publicName: "prefixIcon", isSignal: true, isRequired: false, transformFunction: null }, prefixIconLibrary: { classPropertyName: "prefixIconLibrary", publicName: "prefixIconLibrary", isSignal: true, isRequired: false, transformFunction: null }, suffixIcon: { classPropertyName: "suffixIcon", publicName: "suffixIcon", isSignal: true, isRequired: false, transformFunction: null }, suffixIconLibrary: { classPropertyName: "suffixIconLibrary", publicName: "suffixIconLibrary", isSignal: true, isRequired: false, transformFunction: null }, suffixIconAriaLabel: { classPropertyName: "suffixIconAriaLabel", publicName: "suffixIconAriaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSuffixAction: "onSuffixAction" }, providers: [
|
|
2320
2427
|
{
|
|
2321
2428
|
provide: NG_VALUE_ACCESSOR,
|
|
2322
2429
|
useExisting: forwardRef(() => TnInputComponent),
|
|
2323
2430
|
multi: true
|
|
2324
2431
|
}
|
|
2325
|
-
], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-input-container\"\n [class.tn-input-container--has-prefix]=\"hasPrefixIcon()\"\n [class.tn-input-container--has-suffix]=\"hasSuffixIcon()\"\n>\n <!-- Prefix icon -->\n @if (hasPrefixIcon()) {\n <tn-icon\n class=\"tn-input__prefix-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"prefixIcon()!\"\n [library]=\"prefixIconLibrary()\"\n />\n }\n\n <!-- Regular input field -->\n @if (!
|
|
2432
|
+
], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-input-container\"\n [class.tn-input-container--has-prefix]=\"hasPrefixIcon()\"\n [class.tn-input-container--has-suffix]=\"hasSuffixIcon()\"\n>\n <!-- Prefix icon -->\n @if (hasPrefixIcon()) {\n <tn-icon\n class=\"tn-input__prefix-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"prefixIcon()!\"\n [library]=\"prefixIconLibrary()\"\n />\n }\n\n <!-- Regular input field -->\n @if (!showTextarea()) {\n <input\n #inputEl\n class=\"tn-input\"\n [id]=\"id\"\n [value]=\"value\"\n [type]=\"resolvedType()\"\n [attr.inputmode]=\"numericInputMode()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [tnTestId]=\"testId()\"\n [disabled]=\"isDisabled()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n />\n }\n\n <!-- Textarea field -->\n @if (showTextarea()) {\n <textarea\n #inputEl\n class=\"tn-input tn-textarea\"\n [id]=\"id\"\n [value]=\"value\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [tnTestId]=\"testId()\"\n [rows]=\"rows()\"\n [disabled]=\"isDisabled()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n ></textarea>\n }\n\n <!-- Suffix icon action button -->\n @if (hasSuffixIcon()) {\n <button\n type=\"button\"\n class=\"tn-input__suffix-action\"\n [attr.aria-label]=\"suffixIconAriaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"onSuffixAction.emit($event)\"\n >\n <tn-icon\n size=\"sm\"\n [name]=\"suffixIcon()!\"\n [library]=\"suffixIconLibrary()\"\n />\n </button>\n }\n</div>\n", styles: [".tn-input-container{position:relative;display:flex;align-items:center;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;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-input-container:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-input-container:has(.tn-input:disabled){background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-input-container:has(.tn-input.error){border-color:var(--tn-error, #dc3545)}.tn-input-container:has(.tn-input.error):focus-within{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-input{display:block;flex:1;min-width:0;min-height:2.5rem;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;border-radius:.375rem;outline:none;box-sizing:border-box}.tn-input::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-input:disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed}.tn-input-container--has-prefix .tn-input{padding-left:.75rem;border-left:1px solid var(--tn-lines, #d1d5db);border-top-left-radius:0;border-bottom-left-radius:0}.tn-input-container--has-suffix .tn-input{padding-right:.25rem}.tn-input__prefix-icon{flex-shrink:0;margin-left:.75rem;margin-right:.75rem;color:var(--tn-fg2, #6c757d);pointer-events:none}.tn-input__suffix-action{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:.375rem;padding:.25rem;border:none;border-radius:.25rem;background:transparent;color:var(--tn-fg2, #6c757d);cursor:pointer;transition:background-color .15s ease-in-out,color .15s ease-in-out}.tn-input__suffix-action:hover:not(:disabled){background-color:var(--tn-bg3, #f3f4f6);color:var(--tn-fg1, #1f2937)}.tn-input__suffix-action:focus-visible{outline:2px solid var(--tn-primary, #2563eb);outline-offset:1px}.tn-input__suffix-action:disabled{cursor:not-allowed;pointer-events:none}.tn-textarea{min-height:6rem;resize:vertical;line-height:1.4}@media(prefers-reduced-motion:reduce){.tn-input-container,.tn-input__suffix-action{transition:none}}@media(prefers-contrast:high){.tn-input-container{border-width:2px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: A11yModule }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "fullSize", "customSize"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId"] }] });
|
|
2326
2433
|
}
|
|
2327
2434
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnInputComponent, decorators: [{
|
|
2328
2435
|
type: Component,
|
|
@@ -2332,8 +2439,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
|
|
|
2332
2439
|
useExisting: forwardRef(() => TnInputComponent),
|
|
2333
2440
|
multi: true
|
|
2334
2441
|
}
|
|
2335
|
-
], template: "<div\n class=\"tn-input-container\"\n [class.tn-input-container--has-prefix]=\"hasPrefixIcon()\"\n [class.tn-input-container--has-suffix]=\"hasSuffixIcon()\"\n>\n <!-- Prefix icon -->\n @if (hasPrefixIcon()) {\n <tn-icon\n class=\"tn-input__prefix-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"prefixIcon()!\"\n [library]=\"prefixIconLibrary()\"\n />\n }\n\n <!-- Regular input field -->\n @if (!
|
|
2336
|
-
}], propDecorators: { inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], inputType: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputType", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], multiline: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiline", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], prefixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIcon", required: false }] }], prefixIconLibrary: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIconLibrary", required: false }] }], suffixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIcon", required: false }] }], suffixIconLibrary: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIconLibrary", required: false }] }], suffixIconAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIconAriaLabel", required: false }] }], onSuffixAction: [{ type: i0.Output, args: ["onSuffixAction"] }] } });
|
|
2442
|
+
], template: "<div\n class=\"tn-input-container\"\n [class.tn-input-container--has-prefix]=\"hasPrefixIcon()\"\n [class.tn-input-container--has-suffix]=\"hasSuffixIcon()\"\n>\n <!-- Prefix icon -->\n @if (hasPrefixIcon()) {\n <tn-icon\n class=\"tn-input__prefix-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"prefixIcon()!\"\n [library]=\"prefixIconLibrary()\"\n />\n }\n\n <!-- Regular input field -->\n @if (!showTextarea()) {\n <input\n #inputEl\n class=\"tn-input\"\n [id]=\"id\"\n [value]=\"value\"\n [type]=\"resolvedType()\"\n [attr.inputmode]=\"numericInputMode()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [tnTestId]=\"testId()\"\n [disabled]=\"isDisabled()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n />\n }\n\n <!-- Textarea field -->\n @if (showTextarea()) {\n <textarea\n #inputEl\n class=\"tn-input tn-textarea\"\n [id]=\"id\"\n [value]=\"value\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [tnTestId]=\"testId()\"\n [rows]=\"rows()\"\n [disabled]=\"isDisabled()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n ></textarea>\n }\n\n <!-- Suffix icon action button -->\n @if (hasSuffixIcon()) {\n <button\n type=\"button\"\n class=\"tn-input__suffix-action\"\n [attr.aria-label]=\"suffixIconAriaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"onSuffixAction.emit($event)\"\n >\n <tn-icon\n size=\"sm\"\n [name]=\"suffixIcon()!\"\n [library]=\"suffixIconLibrary()\"\n />\n </button>\n }\n</div>\n", styles: [".tn-input-container{position:relative;display:flex;align-items:center;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;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-input-container:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-input-container:has(.tn-input:disabled){background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-input-container:has(.tn-input.error){border-color:var(--tn-error, #dc3545)}.tn-input-container:has(.tn-input.error):focus-within{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-input{display:block;flex:1;min-width:0;min-height:2.5rem;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;border-radius:.375rem;outline:none;box-sizing:border-box}.tn-input::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-input:disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed}.tn-input-container--has-prefix .tn-input{padding-left:.75rem;border-left:1px solid var(--tn-lines, #d1d5db);border-top-left-radius:0;border-bottom-left-radius:0}.tn-input-container--has-suffix .tn-input{padding-right:.25rem}.tn-input__prefix-icon{flex-shrink:0;margin-left:.75rem;margin-right:.75rem;color:var(--tn-fg2, #6c757d);pointer-events:none}.tn-input__suffix-action{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:.375rem;padding:.25rem;border:none;border-radius:.25rem;background:transparent;color:var(--tn-fg2, #6c757d);cursor:pointer;transition:background-color .15s ease-in-out,color .15s ease-in-out}.tn-input__suffix-action:hover:not(:disabled){background-color:var(--tn-bg3, #f3f4f6);color:var(--tn-fg1, #1f2937)}.tn-input__suffix-action:focus-visible{outline:2px solid var(--tn-primary, #2563eb);outline-offset:1px}.tn-input__suffix-action:disabled{cursor:not-allowed;pointer-events:none}.tn-textarea{min-height:6rem;resize:vertical;line-height:1.4}@media(prefers-reduced-motion:reduce){.tn-input-container,.tn-input__suffix-action{transition:none}}@media(prefers-contrast:high){.tn-input-container{border-width:2px}}\n"] }]
|
|
2443
|
+
}], propDecorators: { inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], inputType: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputType", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], multiline: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiline", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], allowDecimals: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDecimals", required: false }] }], prefixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIcon", required: false }] }], prefixIconLibrary: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIconLibrary", required: false }] }], suffixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIcon", required: false }] }], suffixIconLibrary: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIconLibrary", required: false }] }], suffixIconAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIconAriaLabel", required: false }] }], onSuffixAction: [{ type: i0.Output, args: ["onSuffixAction"] }] } });
|
|
2337
2444
|
|
|
2338
2445
|
/**
|
|
2339
2446
|
* Harness for interacting with tn-icon in tests.
|
|
@@ -2616,6 +2723,59 @@ class TnInputHarness extends ComponentHarness {
|
|
|
2616
2723
|
await input.clear();
|
|
2617
2724
|
await input.sendKeys(value);
|
|
2618
2725
|
}
|
|
2726
|
+
/**
|
|
2727
|
+
* Gets the current value parsed as a number, mirroring what the control emits in number mode.
|
|
2728
|
+
* Empty or non-numeric input resolves to null (never 0).
|
|
2729
|
+
*
|
|
2730
|
+
* @returns Promise resolving to the numeric value, or null.
|
|
2731
|
+
*/
|
|
2732
|
+
async getNumericValue() {
|
|
2733
|
+
const raw = await this.getValue();
|
|
2734
|
+
if (raw === '' || raw === '-' || raw === '.' || raw === '-.') {
|
|
2735
|
+
return null;
|
|
2736
|
+
}
|
|
2737
|
+
// Mirror the control's parse exactly: integer mode (inputmode="numeric") parses
|
|
2738
|
+
// via parseInt, decimal mode via parseFloat. Reading inputmode rather than always
|
|
2739
|
+
// using parseFloat keeps the harness and component from disagreeing on a string
|
|
2740
|
+
// like "3.5" in integer mode (component emits 3; parseFloat would say 3.5).
|
|
2741
|
+
// Both avoid Number()'s coercions (hex/whitespace) the control never emits.
|
|
2742
|
+
const integerMode = (await this.getInputMode()) === 'numeric';
|
|
2743
|
+
const parsed = integerMode ? parseInt(raw, 10) : parseFloat(raw);
|
|
2744
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Gets the `inputmode` attribute (e.g. 'numeric' or 'decimal' in number mode).
|
|
2748
|
+
*
|
|
2749
|
+
* @returns Promise resolving to the inputmode string, or null if unset.
|
|
2750
|
+
*/
|
|
2751
|
+
async getInputMode() {
|
|
2752
|
+
const input = await this.inputEl();
|
|
2753
|
+
return input.getAttribute('inputmode');
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Whether the field is in integer-only mode (i.e. `allowDecimals` is false).
|
|
2757
|
+
*
|
|
2758
|
+
* Derived from `inputmode`: number mode renders `numeric` for integers and
|
|
2759
|
+
* `decimal` otherwise. A convenience wrapper so tests need not know that mapping.
|
|
2760
|
+
*
|
|
2761
|
+
* @returns Promise resolving to true when only whole numbers are accepted.
|
|
2762
|
+
*/
|
|
2763
|
+
async isIntegerOnly() {
|
|
2764
|
+
return (await this.getInputMode()) === 'numeric';
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* Gets the `aria-label` (accessible name set via the `ariaLabel` input).
|
|
2768
|
+
*
|
|
2769
|
+
* @returns Promise resolving to the aria-label string, or null if unset.
|
|
2770
|
+
*/
|
|
2771
|
+
async getAriaLabel() {
|
|
2772
|
+
if (await this.isMultiline()) {
|
|
2773
|
+
const textarea = await this.textareaEl();
|
|
2774
|
+
return textarea.getAttribute('aria-label');
|
|
2775
|
+
}
|
|
2776
|
+
const input = await this.inputEl();
|
|
2777
|
+
return input.getAttribute('aria-label');
|
|
2778
|
+
}
|
|
2619
2779
|
/**
|
|
2620
2780
|
* Gets the placeholder text.
|
|
2621
2781
|
*
|
|
@@ -5454,6 +5614,10 @@ class TnFormFieldComponent {
|
|
|
5454
5614
|
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
5455
5615
|
testId = input('', ...(ngDevMode ? [{ debugName: "testId" }] : []));
|
|
5456
5616
|
subscriptSizing = input('dynamic', ...(ngDevMode ? [{ debugName: "subscriptSizing" }] : []));
|
|
5617
|
+
/** Optional tooltip shown via a help icon next to the label. */
|
|
5618
|
+
tooltip = input('', ...(ngDevMode ? [{ debugName: "tooltip" }] : []));
|
|
5619
|
+
/** Placement of the tooltip relative to its help icon. */
|
|
5620
|
+
tooltipPosition = input('above', ...(ngDevMode ? [{ debugName: "tooltipPosition" }] : []));
|
|
5457
5621
|
control = contentChild(NgControl, ...(ngDevMode ? [{ debugName: "control" }] : []));
|
|
5458
5622
|
hasError = signal(false, ...(ngDevMode ? [{ debugName: "hasError" }] : []));
|
|
5459
5623
|
errorMessage = signal('', ...(ngDevMode ? [{ debugName: "errorMessage" }] : []));
|
|
@@ -5521,12 +5685,12 @@ class TnFormFieldComponent {
|
|
|
5521
5685
|
return this.subscriptSizing() === 'fixed' || this.showError() || this.showHint();
|
|
5522
5686
|
}, ...(ngDevMode ? [{ debugName: "showSubscript" }] : []));
|
|
5523
5687
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFormFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5524
|
-
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
|
|
5688
|
+
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"] }] });
|
|
5525
5689
|
}
|
|
5526
5690
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFormFieldComponent, decorators: [{
|
|
5527
5691
|
type: Component,
|
|
5528
|
-
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
|
|
5529
|
-
}], 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 }] }] } });
|
|
5692
|
+
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"] }]
|
|
5693
|
+
}], 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 }] }] } });
|
|
5530
5694
|
|
|
5531
5695
|
/**
|
|
5532
5696
|
* Harness for interacting with `tn-form-field` in tests.
|
|
@@ -5559,6 +5723,7 @@ class TnFormFieldHarness extends ComponentHarness {
|
|
|
5559
5723
|
_label = this.locatorForOptional('.tn-form-field-label');
|
|
5560
5724
|
_error = this.locatorForOptional('.tn-form-field-error');
|
|
5561
5725
|
_hint = this.locatorForOptional('.tn-form-field-hint');
|
|
5726
|
+
_tooltip = this.locatorForOptional('.tn-form-field-tooltip');
|
|
5562
5727
|
/**
|
|
5563
5728
|
* Gets a `HarnessPredicate` that can be used to search for a form field
|
|
5564
5729
|
* with specific attributes.
|
|
@@ -5650,6 +5815,36 @@ class TnFormFieldHarness extends ComponentHarness {
|
|
|
5650
5815
|
const hint = await this._hint();
|
|
5651
5816
|
return hint ? (await hint.text()).trim() : null;
|
|
5652
5817
|
}
|
|
5818
|
+
/**
|
|
5819
|
+
* Checks whether the form field has a tooltip help icon.
|
|
5820
|
+
*
|
|
5821
|
+
* @returns Promise resolving to true if the tooltip trigger is present.
|
|
5822
|
+
*
|
|
5823
|
+
* @example
|
|
5824
|
+
* ```typescript
|
|
5825
|
+
* const field = await loader.getHarness(TnFormFieldHarness.with({ label: 'Purpose' }));
|
|
5826
|
+
* expect(await field.hasTooltip()).toBe(true);
|
|
5827
|
+
* ```
|
|
5828
|
+
*/
|
|
5829
|
+
async hasTooltip() {
|
|
5830
|
+
const tooltip = await this._tooltip();
|
|
5831
|
+
return tooltip !== null;
|
|
5832
|
+
}
|
|
5833
|
+
/**
|
|
5834
|
+
* Gets the tooltip message (read from the trigger's accessible label).
|
|
5835
|
+
*
|
|
5836
|
+
* @returns Promise resolving to the tooltip text, or null if no tooltip.
|
|
5837
|
+
*
|
|
5838
|
+
* @example
|
|
5839
|
+
* ```typescript
|
|
5840
|
+
* const field = await loader.getHarness(TnFormFieldHarness.with({ label: 'Purpose' }));
|
|
5841
|
+
* expect(await field.getTooltip()).toBe('What this share is used for');
|
|
5842
|
+
* ```
|
|
5843
|
+
*/
|
|
5844
|
+
async getTooltip() {
|
|
5845
|
+
const tooltip = await this._tooltip();
|
|
5846
|
+
return tooltip ? tooltip.getAttribute('aria-label') : null;
|
|
5847
|
+
}
|
|
5653
5848
|
/**
|
|
5654
5849
|
* Checks whether the form field is marked as required.
|
|
5655
5850
|
*
|