@truenas/ui-components 0.1.64 → 0.1.65
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
|
*
|