@keepui/ui 0.2.0 → 0.4.0
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.
- package/README.md +299 -131
- package/fesm2022/keepui-ui.mjs +1240 -13
- package/fesm2022/keepui-ui.mjs.map +1 -1
- package/lib/components/button/button.component.d.ts.map +1 -1
- package/lib/components/card/card.component.d.ts +1 -3
- package/lib/components/card/card.component.d.ts.map +1 -1
- package/lib/components/card/card.types.d.ts +4 -0
- package/lib/components/card/card.types.d.ts.map +1 -0
- package/lib/components/icon/icon.component.d.ts +39 -0
- package/lib/components/icon/icon.component.d.ts.map +1 -0
- package/lib/components/icon-action-button/icon-action-button.component.d.ts +52 -0
- package/lib/components/icon-action-button/icon-action-button.component.d.ts.map +1 -0
- package/lib/components/icon-action-button/icon-action-button.types.d.ts +3 -0
- package/lib/components/icon-action-button/icon-action-button.types.d.ts.map +1 -0
- package/lib/components/signal-dropdown/signal-dropdown.component.d.ts +91 -0
- package/lib/components/signal-dropdown/signal-dropdown.component.d.ts.map +1 -0
- package/lib/components/signal-dropdown/signal-dropdown.types.d.ts +12 -0
- package/lib/components/signal-dropdown/signal-dropdown.types.d.ts.map +1 -0
- package/lib/components/signal-text-input/signal-text-input.component.d.ts +101 -0
- package/lib/components/signal-text-input/signal-text-input.component.d.ts.map +1 -0
- package/lib/components/signal-text-input/signal-text-input.types.d.ts +8 -0
- package/lib/components/signal-text-input/signal-text-input.types.d.ts.map +1 -0
- package/lib/components/signal-textarea/signal-textarea.component.d.ts +70 -0
- package/lib/components/signal-textarea/signal-textarea.component.d.ts.map +1 -0
- package/lib/components/signal-textarea/signal-textarea.types.d.ts +11 -0
- package/lib/components/signal-textarea/signal-textarea.types.d.ts.map +1 -0
- package/lib/i18n/keep-ui-translations.d.ts +4 -0
- package/lib/i18n/keep-ui-translations.d.ts.map +1 -1
- package/lib/i18n/translation-keys.d.ts +4 -0
- package/lib/i18n/translation-keys.d.ts.map +1 -1
- package/package.json +1 -1
- package/public-api.d.ts +10 -0
- package/public-api.d.ts.map +1 -1
- package/styles/index.css +11 -1
- package/styles/prebuilt.css +1 -1
- package/styles/themes.css +27 -0
package/fesm2022/keepui-ui.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, input, output, computed, ChangeDetectionStrategy, Component, inject, signal, makeEnvironmentProviders } from '@angular/core';
|
|
2
|
+
import { InjectionToken, Injectable, input, output, computed, ChangeDetectionStrategy, Component, inject, signal, DestroyRef, viewChild, model, effect, makeEnvironmentProviders } from '@angular/core';
|
|
3
3
|
import { TranslocoPipe, TRANSLOCO_SCOPE, TranslocoService, provideTransloco } from '@jsverse/transloco';
|
|
4
4
|
import { of } from 'rxjs';
|
|
5
5
|
|
|
@@ -137,25 +137,24 @@ class ButtonComponent {
|
|
|
137
137
|
};
|
|
138
138
|
const variantMap = {
|
|
139
139
|
primary: [
|
|
140
|
-
'bg-ku-primary text-
|
|
141
|
-
'enabled:hover:
|
|
142
|
-
'enabled:active:
|
|
140
|
+
'bg-ku-action-primary text-white border border-ku-action-primary',
|
|
141
|
+
'enabled:hover:opacity-90',
|
|
142
|
+
'enabled:active:opacity-80',
|
|
143
143
|
].join(' '),
|
|
144
144
|
secondary: [
|
|
145
|
-
'bg-ku-
|
|
146
|
-
'enabled:hover:bg-ku-
|
|
147
|
-
'disabled:text-ku-gray-text',
|
|
145
|
+
'bg-ku-primary text-ku-action-primary border border-ku-action-primary',
|
|
146
|
+
'enabled:hover:bg-ku-action-background transition-colors',
|
|
148
147
|
].join(' '),
|
|
149
148
|
outline: [
|
|
150
|
-
'bg-
|
|
151
|
-
'enabled:hover:
|
|
149
|
+
'bg-ku-primary border border-ku-secondary-border text-ku-gray-text',
|
|
150
|
+
'enabled:hover:text-ku-action-primary enabled:hover:border-ku-action-primary transition-colors',
|
|
152
151
|
].join(' '),
|
|
153
152
|
ghost: [
|
|
154
|
-
'bg-transparent border border-ku-primary
|
|
155
|
-
'enabled:hover:bg-ku-
|
|
153
|
+
'bg-transparent border border-ku-action-primary text-ku-action-primary',
|
|
154
|
+
'enabled:hover:bg-ku-action-background',
|
|
156
155
|
].join(' '),
|
|
157
156
|
danger: [
|
|
158
|
-
'bg-ku-
|
|
157
|
+
'bg-ku-error-primary text-white border border-ku-error-primary',
|
|
159
158
|
'enabled:hover:opacity-90',
|
|
160
159
|
'enabled:active:opacity-80',
|
|
161
160
|
].join(' '),
|
|
@@ -349,6 +348,189 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
|
|
|
349
348
|
}]
|
|
350
349
|
}] });
|
|
351
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Generic SVG sprite icon renderer.
|
|
353
|
+
*
|
|
354
|
+
* Renders a `<use href="#name">` reference pointing to an SVG symbol pre-registered
|
|
355
|
+
* in the DOM by the consuming application (e.g. via `IconRegistryService`).
|
|
356
|
+
*
|
|
357
|
+
* Accessibility:
|
|
358
|
+
* - When used decoratively (default), the SVG carries `aria-hidden="true"` automatically.
|
|
359
|
+
* - When used as a standalone meaningful icon, supply an `ariaLabel` — the SVG will
|
|
360
|
+
* receive `role="img"` and `aria-label` accordingly.
|
|
361
|
+
*
|
|
362
|
+
* ```html
|
|
363
|
+
* <!-- Decorative — hidden from screen readers -->
|
|
364
|
+
* <keepui-icon name="check-icon" [size]="20" aria-hidden="true" />
|
|
365
|
+
*
|
|
366
|
+
* <!-- Meaningful standalone icon -->
|
|
367
|
+
* <keepui-icon name="close-icon" ariaLabel="Cerrar" />
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
class IconComponent {
|
|
371
|
+
constructor() {
|
|
372
|
+
/** ID of the SVG symbol to render (without the `#` prefix). */
|
|
373
|
+
this.name = input.required();
|
|
374
|
+
/** Width and height of the icon in pixels. @default 24 */
|
|
375
|
+
this.size = input(24);
|
|
376
|
+
/** `viewBox` attribute forwarded to the `<svg>` element. @default '0 0 24 24' */
|
|
377
|
+
this.viewBox = input('0 0 24 24');
|
|
378
|
+
/**
|
|
379
|
+
* Accessible label for standalone icons.
|
|
380
|
+
* When provided, sets `role="img"` and `aria-label` on the SVG.
|
|
381
|
+
* When omitted, the SVG is marked `aria-hidden="true"` (decorative).
|
|
382
|
+
* @default ''
|
|
383
|
+
*/
|
|
384
|
+
this.ariaLabel = input('');
|
|
385
|
+
this.href = computed(() => `#${this.name()}`);
|
|
386
|
+
}
|
|
387
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: IconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
388
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.20", type: IconComponent, isStandalone: true, selector: "keepui-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, viewBox: { classPropertyName: "viewBox", publicName: "viewBox", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "inline-flex items-center justify-center" }, ngImport: i0, template: `
|
|
389
|
+
<svg
|
|
390
|
+
class="block"
|
|
391
|
+
[attr.width]="size()"
|
|
392
|
+
[attr.height]="size()"
|
|
393
|
+
[attr.viewBox]="viewBox()"
|
|
394
|
+
[attr.aria-hidden]="ariaLabel() ? null : true"
|
|
395
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
396
|
+
[attr.role]="ariaLabel() ? 'img' : null"
|
|
397
|
+
>
|
|
398
|
+
<use [attr.href]="href()" />
|
|
399
|
+
</svg>
|
|
400
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
401
|
+
}
|
|
402
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: IconComponent, decorators: [{
|
|
403
|
+
type: Component,
|
|
404
|
+
args: [{
|
|
405
|
+
selector: 'keepui-icon',
|
|
406
|
+
standalone: true,
|
|
407
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
408
|
+
host: {
|
|
409
|
+
class: 'inline-flex items-center justify-center',
|
|
410
|
+
},
|
|
411
|
+
template: `
|
|
412
|
+
<svg
|
|
413
|
+
class="block"
|
|
414
|
+
[attr.width]="size()"
|
|
415
|
+
[attr.height]="size()"
|
|
416
|
+
[attr.viewBox]="viewBox()"
|
|
417
|
+
[attr.aria-hidden]="ariaLabel() ? null : true"
|
|
418
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
419
|
+
[attr.role]="ariaLabel() ? 'img' : null"
|
|
420
|
+
>
|
|
421
|
+
<use [attr.href]="href()" />
|
|
422
|
+
</svg>
|
|
423
|
+
`,
|
|
424
|
+
}]
|
|
425
|
+
}] });
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Circular icon-only action button with `default` and `danger` variants,
|
|
429
|
+
* loading state, and full accessibility support.
|
|
430
|
+
*
|
|
431
|
+
* Because this button renders no visible text, `ariaLabel` is **required** to
|
|
432
|
+
* satisfy WCAG 2.1 SC 4.1.2 (Name, Role, Value).
|
|
433
|
+
*
|
|
434
|
+
* The icon color is inherited automatically via `currentColor` from the button's
|
|
435
|
+
* text color, so SVG symbols defined with `stroke="currentColor"` will adapt to
|
|
436
|
+
* both variants and themes without extra classes.
|
|
437
|
+
*
|
|
438
|
+
* ```html
|
|
439
|
+
* <keepui-icon-action-button icon="edit-icon" ariaLabel="Editar" />
|
|
440
|
+
*
|
|
441
|
+
* <keepui-icon-action-button
|
|
442
|
+
* icon="trash-icon"
|
|
443
|
+
* variant="danger"
|
|
444
|
+
* ariaLabel="Eliminar elemento"
|
|
445
|
+
* [loading]="isDeleting()"
|
|
446
|
+
* />
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
class IconActionButtonComponent {
|
|
450
|
+
constructor() {
|
|
451
|
+
/** ID of the SVG symbol to render (without the `#` prefix). */
|
|
452
|
+
this.icon = input.required();
|
|
453
|
+
/**
|
|
454
|
+
* Accessible label for the button. Always required — icon-only buttons must
|
|
455
|
+
* have a programmatic name for screen readers (WCAG 2.1 SC 4.1.2).
|
|
456
|
+
*/
|
|
457
|
+
this.ariaLabel = input.required();
|
|
458
|
+
/** Visual style variant. @default 'default' */
|
|
459
|
+
this.variant = input('default');
|
|
460
|
+
/** Size of the inner icon in pixels. @default 20 */
|
|
461
|
+
this.iconSize = input(20);
|
|
462
|
+
/** HTML `type` attribute of the inner `<button>`. @default 'button' */
|
|
463
|
+
this.type = input('button');
|
|
464
|
+
/** Disables the button when `true`. @default false */
|
|
465
|
+
this.disabled = input(false);
|
|
466
|
+
/**
|
|
467
|
+
* Shows an animated spinner and disables the button.
|
|
468
|
+
* Also sets `aria-busy="true"` on the element.
|
|
469
|
+
* @default false
|
|
470
|
+
*/
|
|
471
|
+
this.loading = input(false);
|
|
472
|
+
this.isDisabled = computed(() => this.disabled() || this.loading());
|
|
473
|
+
this.buttonClass = computed(() => {
|
|
474
|
+
const base = [
|
|
475
|
+
'inline-flex items-center justify-center shrink-0 cursor-pointer',
|
|
476
|
+
'size-11 rounded-full border transition-colors',
|
|
477
|
+
'focus-visible:outline-none focus-visible:ring-2',
|
|
478
|
+
'focus-visible:ring-ku-primary-border focus-visible:ring-offset-2',
|
|
479
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
480
|
+
].join(' ');
|
|
481
|
+
const variantMap = {
|
|
482
|
+
default: [
|
|
483
|
+
'border-ku-secondary-border bg-ku-primary text-ku-gray-text',
|
|
484
|
+
'enabled:hover:bg-ku-action-background enabled:hover:border-ku-action-primary',
|
|
485
|
+
'enabled:hover:text-ku-action-primary',
|
|
486
|
+
].join(' '),
|
|
487
|
+
danger: [
|
|
488
|
+
'border-ku-secondary-border bg-ku-primary text-ku-gray-text',
|
|
489
|
+
'enabled:hover:bg-ku-error-background enabled:hover:border-ku-error-primary enabled:hover:text-ku-error-primary',
|
|
490
|
+
'enabled:hover:opacity-90',
|
|
491
|
+
].join(' '),
|
|
492
|
+
};
|
|
493
|
+
return `${base} ${variantMap[this.variant()]}`;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: IconActionButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
497
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: IconActionButtonComponent, isStandalone: true, selector: "keepui-icon-action-button", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: true, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, iconSize: { classPropertyName: "iconSize", publicName: "iconSize", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
498
|
+
<button
|
|
499
|
+
[attr.type]="type()"
|
|
500
|
+
[disabled]="isDisabled()"
|
|
501
|
+
[attr.aria-disabled]="isDisabled() ? true : null"
|
|
502
|
+
[attr.aria-busy]="loading() ? true : null"
|
|
503
|
+
[attr.aria-label]="ariaLabel()"
|
|
504
|
+
[class]="buttonClass()"
|
|
505
|
+
>
|
|
506
|
+
@if (loading()) {
|
|
507
|
+
<span class="keepui-iab-spinner" aria-hidden="true"></span>
|
|
508
|
+
} @else {
|
|
509
|
+
<keepui-icon [name]="icon()" [size]="iconSize()" />
|
|
510
|
+
}
|
|
511
|
+
</button>
|
|
512
|
+
`, isInline: true, styles: [".keepui-iab-spinner{display:inline-block;width:1.25em;height:1.25em;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:keepui-iab-spin .65s linear infinite;flex-shrink:0}@keyframes keepui-iab-spin{to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "component", type: IconComponent, selector: "keepui-icon", inputs: ["name", "size", "viewBox", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
513
|
+
}
|
|
514
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: IconActionButtonComponent, decorators: [{
|
|
515
|
+
type: Component,
|
|
516
|
+
args: [{ selector: 'keepui-icon-action-button', standalone: true, imports: [IconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
517
|
+
<button
|
|
518
|
+
[attr.type]="type()"
|
|
519
|
+
[disabled]="isDisabled()"
|
|
520
|
+
[attr.aria-disabled]="isDisabled() ? true : null"
|
|
521
|
+
[attr.aria-busy]="loading() ? true : null"
|
|
522
|
+
[attr.aria-label]="ariaLabel()"
|
|
523
|
+
[class]="buttonClass()"
|
|
524
|
+
>
|
|
525
|
+
@if (loading()) {
|
|
526
|
+
<span class="keepui-iab-spinner" aria-hidden="true"></span>
|
|
527
|
+
} @else {
|
|
528
|
+
<keepui-icon [name]="icon()" [size]="iconSize()" />
|
|
529
|
+
}
|
|
530
|
+
</button>
|
|
531
|
+
`, styles: [".keepui-iab-spinner{display:inline-block;width:1.25em;height:1.25em;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:keepui-iab-spin .65s linear infinite;flex-shrink:0}@keyframes keepui-iab-spin{to{transform:rotate(360deg)}}\n"] }]
|
|
532
|
+
}] });
|
|
533
|
+
|
|
352
534
|
/**
|
|
353
535
|
* Typed constants for every translation key used in @keepui/ui.
|
|
354
536
|
*
|
|
@@ -369,6 +551,10 @@ const KEEPUI_TRANSLATION_KEYS = {
|
|
|
369
551
|
PREVIEW_ALT: 'imagePreview.previewAlt',
|
|
370
552
|
ERROR_UNEXPECTED: 'imagePreview.errorUnexpected',
|
|
371
553
|
},
|
|
554
|
+
SIGNAL_TEXT_INPUT: {
|
|
555
|
+
SHOW_PASSWORD: 'signalTextInput.showPassword',
|
|
556
|
+
HIDE_PASSWORD: 'signalTextInput.hidePassword',
|
|
557
|
+
},
|
|
372
558
|
};
|
|
373
559
|
/** Ordered list of languages available in KeepUI. */
|
|
374
560
|
const KEEPUI_AVAILABLE_LANGUAGES = [
|
|
@@ -508,6 +694,1035 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
|
|
|
508
694
|
}]
|
|
509
695
|
}] });
|
|
510
696
|
|
|
697
|
+
const DROPDOWN_ITEM_HEIGHT = 40;
|
|
698
|
+
const DROPDOWN_PADDING = 8;
|
|
699
|
+
const DROPDOWN_GAP = 8;
|
|
700
|
+
/**
|
|
701
|
+
* Signal-based accessible dropdown / select component.
|
|
702
|
+
*
|
|
703
|
+
* Fully platform-agnostic — no native API usage. The panel opens in `fixed`
|
|
704
|
+
* position so it is never clipped by overflow-hidden ancestors. It repositions
|
|
705
|
+
* itself automatically on scroll and resize.
|
|
706
|
+
*
|
|
707
|
+
* The `value` and `touched` properties are `model()` signals so the component
|
|
708
|
+
* integrates seamlessly with Angular signal-based forms.
|
|
709
|
+
*
|
|
710
|
+
* ```html
|
|
711
|
+
* <keepui-signal-dropdown
|
|
712
|
+
* label="País"
|
|
713
|
+
* placeholder="Selecciona un país"
|
|
714
|
+
* [options]="countries"
|
|
715
|
+
* [(value)]="selectedCountry"
|
|
716
|
+
* />
|
|
717
|
+
* ```
|
|
718
|
+
*
|
|
719
|
+
* @typeParam T – type of each option's `value` property. Defaults to `string`.
|
|
720
|
+
*/
|
|
721
|
+
class SignalDropdownComponent {
|
|
722
|
+
constructor() {
|
|
723
|
+
this.destroyRef = inject(DestroyRef);
|
|
724
|
+
this.isOpen = signal(false);
|
|
725
|
+
this.openUpwards = signal(false);
|
|
726
|
+
this.panelTopPx = signal(0);
|
|
727
|
+
this.panelLeftPx = signal(0);
|
|
728
|
+
this.panelWidthPx = signal(0);
|
|
729
|
+
this.dropdownButton = viewChild('dropdownButton');
|
|
730
|
+
this.dropdownContainer = viewChild('dropdownContainer');
|
|
731
|
+
/** Optional label text rendered above the dropdown. */
|
|
732
|
+
this.label = input('');
|
|
733
|
+
/** Placeholder shown when no value is selected. */
|
|
734
|
+
this.placeholder = input('');
|
|
735
|
+
/** Array of options to display in the panel. */
|
|
736
|
+
this.options = input.required();
|
|
737
|
+
/** Layout width of the wrapper. @default 'full' */
|
|
738
|
+
this.width = input('full');
|
|
739
|
+
/** Marks the field as required. Adds `aria-required` and a visual asterisk. @default false */
|
|
740
|
+
this.required = input(false);
|
|
741
|
+
/** Human-readable error message shown below the dropdown. Takes precedence over `errors[0]`. */
|
|
742
|
+
this.errorMessage = input('');
|
|
743
|
+
/**
|
|
744
|
+
* Array of error strings. The first item is displayed when `errorMessage` is empty.
|
|
745
|
+
* Set together with `invalid=true` to trigger the error state.
|
|
746
|
+
*/
|
|
747
|
+
this.errors = input([]);
|
|
748
|
+
/**
|
|
749
|
+
* Stable `id` used to link the `<label>` with the trigger `<button>`.
|
|
750
|
+
* A random suffix is generated by default.
|
|
751
|
+
*/
|
|
752
|
+
this.selectId = input(`ku-dropdown-${Math.random().toString(36).slice(2, 8)}`);
|
|
753
|
+
/** Disables the dropdown. @default false */
|
|
754
|
+
this.disabled = input(false);
|
|
755
|
+
/**
|
|
756
|
+
* Forces the error visual state regardless of the `touched` model.
|
|
757
|
+
* Useful for external form validation. @default false
|
|
758
|
+
*/
|
|
759
|
+
this.invalid = input(false);
|
|
760
|
+
/** Currently selected value. Use `[(value)]` for two-way binding. */
|
|
761
|
+
this.value = model(null);
|
|
762
|
+
/** Whether the control has been interacted with. Use `[(touched)]` for two-way binding. */
|
|
763
|
+
this.touched = model(false);
|
|
764
|
+
/** Emitted when the selected value changes. */
|
|
765
|
+
this.valueChange = output();
|
|
766
|
+
this.errorId = computed(() => `${this.selectId()}-error`);
|
|
767
|
+
this.panelStyle = computed(() => `top:${this.panelTopPx()}px;left:${this.panelLeftPx()}px;width:${this.panelWidthPx()}px`);
|
|
768
|
+
this.widthClass = computed(() => {
|
|
769
|
+
const map = {
|
|
770
|
+
full: 'w-full',
|
|
771
|
+
half: 'w-1/2',
|
|
772
|
+
auto: 'w-auto',
|
|
773
|
+
};
|
|
774
|
+
return map[this.width()];
|
|
775
|
+
});
|
|
776
|
+
this.selectedLabel = computed(() => {
|
|
777
|
+
const current = this.value();
|
|
778
|
+
if (current === null)
|
|
779
|
+
return this.placeholder();
|
|
780
|
+
return this.options().find(o => o.value === current)?.label ?? this.placeholder();
|
|
781
|
+
});
|
|
782
|
+
this.selectedBadges = computed(() => {
|
|
783
|
+
const current = this.value();
|
|
784
|
+
if (current === null)
|
|
785
|
+
return [];
|
|
786
|
+
return this.options().find(o => o.value === current)?.badges ?? [];
|
|
787
|
+
});
|
|
788
|
+
this.showError = computed(() => this.touched() && (this.invalid() || this.errorMessage().length > 0 || this.errors().length > 0));
|
|
789
|
+
this.recalculatePosition = () => {
|
|
790
|
+
if (this.isOpen())
|
|
791
|
+
this.calculateDropdownPosition();
|
|
792
|
+
};
|
|
793
|
+
effect(() => {
|
|
794
|
+
if (this.isOpen()) {
|
|
795
|
+
this.calculateDropdownPosition();
|
|
796
|
+
this.addViewportListeners();
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
this.removeViewportListeners();
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
this.destroyRef.onDestroy(() => this.removeViewportListeners());
|
|
803
|
+
}
|
|
804
|
+
onDocumentClick(event) {
|
|
805
|
+
const container = this.dropdownContainer()?.nativeElement;
|
|
806
|
+
if (container && !container.contains(event.target)) {
|
|
807
|
+
this.isOpen.set(false);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
toggleDropdown() {
|
|
811
|
+
if (this.disabled())
|
|
812
|
+
return;
|
|
813
|
+
this.isOpen.update(open => !open);
|
|
814
|
+
}
|
|
815
|
+
close() {
|
|
816
|
+
if (this.isOpen()) {
|
|
817
|
+
this.isOpen.set(false);
|
|
818
|
+
this.dropdownButton()?.nativeElement.focus();
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
selectOption(option) {
|
|
822
|
+
this.value.set(option.value);
|
|
823
|
+
this.isOpen.set(false);
|
|
824
|
+
this.dropdownButton()?.nativeElement.focus();
|
|
825
|
+
this.valueChange.emit(option.value);
|
|
826
|
+
}
|
|
827
|
+
onBlur() {
|
|
828
|
+
this.touched.set(true);
|
|
829
|
+
}
|
|
830
|
+
isSelected(optionValue) {
|
|
831
|
+
return this.value() === optionValue;
|
|
832
|
+
}
|
|
833
|
+
onButtonKeydown(event) {
|
|
834
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
835
|
+
event.preventDefault();
|
|
836
|
+
if (!this.isOpen()) {
|
|
837
|
+
this.isOpen.set(true);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
onOptionKeydown(event, option, index) {
|
|
842
|
+
const panel = this.dropdownContainer()?.nativeElement;
|
|
843
|
+
const optionButtons = panel?.querySelectorAll('[role="option"]');
|
|
844
|
+
if (!optionButtons)
|
|
845
|
+
return;
|
|
846
|
+
if (event.key === 'ArrowDown') {
|
|
847
|
+
event.preventDefault();
|
|
848
|
+
optionButtons[Math.min(index + 1, optionButtons.length - 1)]?.focus();
|
|
849
|
+
}
|
|
850
|
+
else if (event.key === 'ArrowUp') {
|
|
851
|
+
event.preventDefault();
|
|
852
|
+
if (index === 0) {
|
|
853
|
+
this.dropdownButton()?.nativeElement.focus();
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
optionButtons[index - 1]?.focus();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
else if (event.key === 'Enter' || event.key === ' ') {
|
|
860
|
+
event.preventDefault();
|
|
861
|
+
this.selectOption(option);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
calculateDropdownPosition() {
|
|
865
|
+
const button = this.dropdownButton()?.nativeElement;
|
|
866
|
+
if (!button)
|
|
867
|
+
return;
|
|
868
|
+
const rect = button.getBoundingClientRect();
|
|
869
|
+
const estimatedHeight = Math.min(this.options().length * DROPDOWN_ITEM_HEIGHT + DROPDOWN_PADDING, 256);
|
|
870
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
871
|
+
const spaceAbove = rect.top;
|
|
872
|
+
const shouldOpenUpwards = spaceBelow < estimatedHeight + DROPDOWN_GAP &&
|
|
873
|
+
spaceAbove > estimatedHeight + DROPDOWN_GAP;
|
|
874
|
+
this.openUpwards.set(shouldOpenUpwards);
|
|
875
|
+
this.panelWidthPx.set(rect.width);
|
|
876
|
+
this.panelLeftPx.set(rect.left);
|
|
877
|
+
this.panelTopPx.set(shouldOpenUpwards
|
|
878
|
+
? rect.top - estimatedHeight - DROPDOWN_GAP
|
|
879
|
+
: rect.bottom + DROPDOWN_GAP);
|
|
880
|
+
}
|
|
881
|
+
addViewportListeners() {
|
|
882
|
+
this.removeViewportListeners();
|
|
883
|
+
window.addEventListener('scroll', this.recalculatePosition, true);
|
|
884
|
+
window.addEventListener('resize', this.recalculatePosition);
|
|
885
|
+
}
|
|
886
|
+
removeViewportListeners() {
|
|
887
|
+
window.removeEventListener('scroll', this.recalculatePosition, true);
|
|
888
|
+
window.removeEventListener('resize', this.recalculatePosition);
|
|
889
|
+
}
|
|
890
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SignalDropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
891
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: SignalDropdownComponent, isStandalone: true, selector: "keepui-signal-dropdown", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, selectId: { classPropertyName: "selectId", publicName: "selectId", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", valueChange: "valueChange" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "close()" }, classAttribute: "block" }, viewQueries: [{ propertyName: "dropdownButton", first: true, predicate: ["dropdownButton"], descendants: true, isSignal: true }, { propertyName: "dropdownContainer", first: true, predicate: ["dropdownContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
892
|
+
<div class="flex flex-col gap-1" [class]="widthClass()">
|
|
893
|
+
|
|
894
|
+
@if (label()) {
|
|
895
|
+
<label [for]="selectId()" class="text-sm text-ku-gray-text">
|
|
896
|
+
{{ label() }}
|
|
897
|
+
@if (required()) {
|
|
898
|
+
<span class="text-ku-error-primary" aria-hidden="true"> *</span>
|
|
899
|
+
}
|
|
900
|
+
</label>
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
<div class="relative" #dropdownContainer>
|
|
904
|
+
|
|
905
|
+
<button
|
|
906
|
+
#dropdownButton
|
|
907
|
+
[id]="selectId()"
|
|
908
|
+
type="button"
|
|
909
|
+
[disabled]="disabled()"
|
|
910
|
+
[attr.aria-disabled]="disabled() ? true : null"
|
|
911
|
+
[attr.aria-expanded]="isOpen()"
|
|
912
|
+
[attr.aria-haspopup]="'listbox'"
|
|
913
|
+
[attr.aria-required]="required() ? true : null"
|
|
914
|
+
[attr.aria-invalid]="showError() ? true : null"
|
|
915
|
+
[attr.aria-describedby]="showError() ? errorId() : null"
|
|
916
|
+
(click)="toggleDropdown()"
|
|
917
|
+
(blur)="onBlur()"
|
|
918
|
+
(keydown)="onButtonKeydown($event)"
|
|
919
|
+
class="w-full inline-flex items-center gap-2 bg-ku-primary border rounded-xl
|
|
920
|
+
px-4 py-2.5 text-sm transition-all cursor-pointer min-h-[2.75rem]
|
|
921
|
+
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ku-action-primary
|
|
922
|
+
focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
923
|
+
[class]="showError() ? 'border-ku-error-primary' : 'border-ku-secondary-border'"
|
|
924
|
+
>
|
|
925
|
+
<span
|
|
926
|
+
class="flex-1 text-left flex items-center justify-between gap-2 min-w-0"
|
|
927
|
+
[class]="value() === null ? 'text-ku-gray-text opacity-70' : 'text-ku-primary-text'"
|
|
928
|
+
>
|
|
929
|
+
<span class="truncate">{{ selectedLabel() }}</span>
|
|
930
|
+
|
|
931
|
+
@if (selectedBadges().length) {
|
|
932
|
+
<span class="flex flex-wrap items-center gap-1.5 shrink-0">
|
|
933
|
+
@for (badge of selectedBadges(); track badge) {
|
|
934
|
+
<span class="inline-flex items-center rounded-full bg-ku-action-background
|
|
935
|
+
px-2 py-0.5 text-[11px] font-medium text-ku-action-primary">
|
|
936
|
+
{{ badge }}
|
|
937
|
+
</span>
|
|
938
|
+
}
|
|
939
|
+
</span>
|
|
940
|
+
}
|
|
941
|
+
</span>
|
|
942
|
+
|
|
943
|
+
<keepui-icon
|
|
944
|
+
name="chevron-down-icon"
|
|
945
|
+
[size]="18"
|
|
946
|
+
aria-hidden="true"
|
|
947
|
+
class="text-ku-gray-text transition-transform duration-200 shrink-0"
|
|
948
|
+
[class.rotate-180]="isOpen()"
|
|
949
|
+
/>
|
|
950
|
+
</button>
|
|
951
|
+
|
|
952
|
+
@if (isOpen()) {
|
|
953
|
+
<div
|
|
954
|
+
role="listbox"
|
|
955
|
+
[attr.aria-label]="label() || null"
|
|
956
|
+
class="fixed rounded-xl shadow-lg bg-ku-primary border border-ku-secondary-border
|
|
957
|
+
z-[1000] overflow-y-auto max-h-64"
|
|
958
|
+
[style]="panelStyle()"
|
|
959
|
+
>
|
|
960
|
+
@for (option of options(); track option.value; let i = $index) {
|
|
961
|
+
<button
|
|
962
|
+
type="button"
|
|
963
|
+
role="option"
|
|
964
|
+
[attr.aria-selected]="isSelected(option.value)"
|
|
965
|
+
(click)="selectOption(option)"
|
|
966
|
+
(keydown)="onOptionKeydown($event, option, i)"
|
|
967
|
+
class="w-full text-left cursor-pointer px-4 py-2 text-sm text-ku-primary-text
|
|
968
|
+
flex items-center gap-3 transition-colors min-h-[2.75rem]
|
|
969
|
+
hover:bg-ku-primary-hover focus-visible:outline-none
|
|
970
|
+
focus-visible:bg-ku-primary-hover"
|
|
971
|
+
[class]="isSelected(option.value) ? 'bg-ku-primary-hover' : ''"
|
|
972
|
+
>
|
|
973
|
+
<span class="flex-1 flex items-center justify-between gap-2 min-w-0">
|
|
974
|
+
<span class="truncate">{{ option.label }}</span>
|
|
975
|
+
|
|
976
|
+
@if (option.badges?.length) {
|
|
977
|
+
<span class="flex flex-wrap items-center gap-1.5 shrink-0">
|
|
978
|
+
@for (badge of option.badges ?? []; track badge) {
|
|
979
|
+
<span class="inline-flex items-center rounded-full bg-ku-action-background
|
|
980
|
+
px-2 py-0.5 text-[11px] font-medium text-ku-action-primary">
|
|
981
|
+
{{ badge }}
|
|
982
|
+
</span>
|
|
983
|
+
}
|
|
984
|
+
</span>
|
|
985
|
+
}
|
|
986
|
+
</span>
|
|
987
|
+
|
|
988
|
+
@if (isSelected(option.value)) {
|
|
989
|
+
<keepui-icon
|
|
990
|
+
name="check-icon"
|
|
991
|
+
[size]="16"
|
|
992
|
+
aria-hidden="true"
|
|
993
|
+
class="text-ku-action-primary shrink-0"
|
|
994
|
+
/>
|
|
995
|
+
}
|
|
996
|
+
</button>
|
|
997
|
+
}
|
|
998
|
+
</div>
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
</div>
|
|
1002
|
+
|
|
1003
|
+
@if (showError()) {
|
|
1004
|
+
<span
|
|
1005
|
+
[id]="errorId()"
|
|
1006
|
+
class="text-sm text-ku-error-primary"
|
|
1007
|
+
role="alert"
|
|
1008
|
+
>
|
|
1009
|
+
@if (errorMessage()) {
|
|
1010
|
+
{{ errorMessage() }}
|
|
1011
|
+
} @else if (errors().length > 0) {
|
|
1012
|
+
{{ errors()[0] }}
|
|
1013
|
+
}
|
|
1014
|
+
</span>
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
</div>
|
|
1018
|
+
`, isInline: true, dependencies: [{ kind: "component", type: IconComponent, selector: "keepui-icon", inputs: ["name", "size", "viewBox", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1019
|
+
}
|
|
1020
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SignalDropdownComponent, decorators: [{
|
|
1021
|
+
type: Component,
|
|
1022
|
+
args: [{
|
|
1023
|
+
selector: 'keepui-signal-dropdown',
|
|
1024
|
+
standalone: true,
|
|
1025
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1026
|
+
imports: [IconComponent],
|
|
1027
|
+
host: {
|
|
1028
|
+
class: 'block',
|
|
1029
|
+
'(document:click)': 'onDocumentClick($event)',
|
|
1030
|
+
'(document:keydown.escape)': 'close()',
|
|
1031
|
+
},
|
|
1032
|
+
template: `
|
|
1033
|
+
<div class="flex flex-col gap-1" [class]="widthClass()">
|
|
1034
|
+
|
|
1035
|
+
@if (label()) {
|
|
1036
|
+
<label [for]="selectId()" class="text-sm text-ku-gray-text">
|
|
1037
|
+
{{ label() }}
|
|
1038
|
+
@if (required()) {
|
|
1039
|
+
<span class="text-ku-error-primary" aria-hidden="true"> *</span>
|
|
1040
|
+
}
|
|
1041
|
+
</label>
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
<div class="relative" #dropdownContainer>
|
|
1045
|
+
|
|
1046
|
+
<button
|
|
1047
|
+
#dropdownButton
|
|
1048
|
+
[id]="selectId()"
|
|
1049
|
+
type="button"
|
|
1050
|
+
[disabled]="disabled()"
|
|
1051
|
+
[attr.aria-disabled]="disabled() ? true : null"
|
|
1052
|
+
[attr.aria-expanded]="isOpen()"
|
|
1053
|
+
[attr.aria-haspopup]="'listbox'"
|
|
1054
|
+
[attr.aria-required]="required() ? true : null"
|
|
1055
|
+
[attr.aria-invalid]="showError() ? true : null"
|
|
1056
|
+
[attr.aria-describedby]="showError() ? errorId() : null"
|
|
1057
|
+
(click)="toggleDropdown()"
|
|
1058
|
+
(blur)="onBlur()"
|
|
1059
|
+
(keydown)="onButtonKeydown($event)"
|
|
1060
|
+
class="w-full inline-flex items-center gap-2 bg-ku-primary border rounded-xl
|
|
1061
|
+
px-4 py-2.5 text-sm transition-all cursor-pointer min-h-[2.75rem]
|
|
1062
|
+
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ku-action-primary
|
|
1063
|
+
focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
1064
|
+
[class]="showError() ? 'border-ku-error-primary' : 'border-ku-secondary-border'"
|
|
1065
|
+
>
|
|
1066
|
+
<span
|
|
1067
|
+
class="flex-1 text-left flex items-center justify-between gap-2 min-w-0"
|
|
1068
|
+
[class]="value() === null ? 'text-ku-gray-text opacity-70' : 'text-ku-primary-text'"
|
|
1069
|
+
>
|
|
1070
|
+
<span class="truncate">{{ selectedLabel() }}</span>
|
|
1071
|
+
|
|
1072
|
+
@if (selectedBadges().length) {
|
|
1073
|
+
<span class="flex flex-wrap items-center gap-1.5 shrink-0">
|
|
1074
|
+
@for (badge of selectedBadges(); track badge) {
|
|
1075
|
+
<span class="inline-flex items-center rounded-full bg-ku-action-background
|
|
1076
|
+
px-2 py-0.5 text-[11px] font-medium text-ku-action-primary">
|
|
1077
|
+
{{ badge }}
|
|
1078
|
+
</span>
|
|
1079
|
+
}
|
|
1080
|
+
</span>
|
|
1081
|
+
}
|
|
1082
|
+
</span>
|
|
1083
|
+
|
|
1084
|
+
<keepui-icon
|
|
1085
|
+
name="chevron-down-icon"
|
|
1086
|
+
[size]="18"
|
|
1087
|
+
aria-hidden="true"
|
|
1088
|
+
class="text-ku-gray-text transition-transform duration-200 shrink-0"
|
|
1089
|
+
[class.rotate-180]="isOpen()"
|
|
1090
|
+
/>
|
|
1091
|
+
</button>
|
|
1092
|
+
|
|
1093
|
+
@if (isOpen()) {
|
|
1094
|
+
<div
|
|
1095
|
+
role="listbox"
|
|
1096
|
+
[attr.aria-label]="label() || null"
|
|
1097
|
+
class="fixed rounded-xl shadow-lg bg-ku-primary border border-ku-secondary-border
|
|
1098
|
+
z-[1000] overflow-y-auto max-h-64"
|
|
1099
|
+
[style]="panelStyle()"
|
|
1100
|
+
>
|
|
1101
|
+
@for (option of options(); track option.value; let i = $index) {
|
|
1102
|
+
<button
|
|
1103
|
+
type="button"
|
|
1104
|
+
role="option"
|
|
1105
|
+
[attr.aria-selected]="isSelected(option.value)"
|
|
1106
|
+
(click)="selectOption(option)"
|
|
1107
|
+
(keydown)="onOptionKeydown($event, option, i)"
|
|
1108
|
+
class="w-full text-left cursor-pointer px-4 py-2 text-sm text-ku-primary-text
|
|
1109
|
+
flex items-center gap-3 transition-colors min-h-[2.75rem]
|
|
1110
|
+
hover:bg-ku-primary-hover focus-visible:outline-none
|
|
1111
|
+
focus-visible:bg-ku-primary-hover"
|
|
1112
|
+
[class]="isSelected(option.value) ? 'bg-ku-primary-hover' : ''"
|
|
1113
|
+
>
|
|
1114
|
+
<span class="flex-1 flex items-center justify-between gap-2 min-w-0">
|
|
1115
|
+
<span class="truncate">{{ option.label }}</span>
|
|
1116
|
+
|
|
1117
|
+
@if (option.badges?.length) {
|
|
1118
|
+
<span class="flex flex-wrap items-center gap-1.5 shrink-0">
|
|
1119
|
+
@for (badge of option.badges ?? []; track badge) {
|
|
1120
|
+
<span class="inline-flex items-center rounded-full bg-ku-action-background
|
|
1121
|
+
px-2 py-0.5 text-[11px] font-medium text-ku-action-primary">
|
|
1122
|
+
{{ badge }}
|
|
1123
|
+
</span>
|
|
1124
|
+
}
|
|
1125
|
+
</span>
|
|
1126
|
+
}
|
|
1127
|
+
</span>
|
|
1128
|
+
|
|
1129
|
+
@if (isSelected(option.value)) {
|
|
1130
|
+
<keepui-icon
|
|
1131
|
+
name="check-icon"
|
|
1132
|
+
[size]="16"
|
|
1133
|
+
aria-hidden="true"
|
|
1134
|
+
class="text-ku-action-primary shrink-0"
|
|
1135
|
+
/>
|
|
1136
|
+
}
|
|
1137
|
+
</button>
|
|
1138
|
+
}
|
|
1139
|
+
</div>
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
</div>
|
|
1143
|
+
|
|
1144
|
+
@if (showError()) {
|
|
1145
|
+
<span
|
|
1146
|
+
[id]="errorId()"
|
|
1147
|
+
class="text-sm text-ku-error-primary"
|
|
1148
|
+
role="alert"
|
|
1149
|
+
>
|
|
1150
|
+
@if (errorMessage()) {
|
|
1151
|
+
{{ errorMessage() }}
|
|
1152
|
+
} @else if (errors().length > 0) {
|
|
1153
|
+
{{ errors()[0] }}
|
|
1154
|
+
}
|
|
1155
|
+
</span>
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
</div>
|
|
1159
|
+
`,
|
|
1160
|
+
}]
|
|
1161
|
+
}], ctorParameters: () => [] });
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Signal-based accessible text input supporting all common HTML input types,
|
|
1165
|
+
* leading/trailing icons, a trailing content slot, and a built-in
|
|
1166
|
+
* password-visibility toggle (when `type="password"`).
|
|
1167
|
+
*
|
|
1168
|
+
* The `value` and `touched` properties are `model()` signals so the component
|
|
1169
|
+
* integrates seamlessly with Angular signal-based forms.
|
|
1170
|
+
*
|
|
1171
|
+
* Password toggle labels are translated via Transloco (scope `'keepui'`).
|
|
1172
|
+
* Call `provideKeepUiI18n()` in your `app.config.ts` to activate i18n.
|
|
1173
|
+
*
|
|
1174
|
+
* ```html
|
|
1175
|
+
* <keepui-signal-text-input
|
|
1176
|
+
* label="Email"
|
|
1177
|
+
* type="email"
|
|
1178
|
+
* placeholder="usuario@ejemplo.com"
|
|
1179
|
+
* leadingIcon="mail-icon"
|
|
1180
|
+
* [(value)]="email"
|
|
1181
|
+
* />
|
|
1182
|
+
*
|
|
1183
|
+
* <!-- Password with toggle -->
|
|
1184
|
+
* <keepui-signal-text-input
|
|
1185
|
+
* label="Contraseña"
|
|
1186
|
+
* type="password"
|
|
1187
|
+
* [(value)]="password"
|
|
1188
|
+
* />
|
|
1189
|
+
* ```
|
|
1190
|
+
*/
|
|
1191
|
+
class SignalTextInputComponent {
|
|
1192
|
+
constructor() {
|
|
1193
|
+
this.isPasswordVisible = signal(false);
|
|
1194
|
+
this.inputRef = viewChild('inputRef');
|
|
1195
|
+
/** Translation key references (typed via KEEPUI_TRANSLATION_KEYS). */
|
|
1196
|
+
this.keys = KEEPUI_TRANSLATION_KEYS.SIGNAL_TEXT_INPUT;
|
|
1197
|
+
/** Optional label text rendered above the input. */
|
|
1198
|
+
this.label = input('');
|
|
1199
|
+
/** Placeholder passed to the underlying `<input>`. */
|
|
1200
|
+
this.placeholder = input('');
|
|
1201
|
+
/** HTML `type` attribute. Use `'password'` to enable the visibility toggle. @default 'text' */
|
|
1202
|
+
this.type = input('text');
|
|
1203
|
+
/** Layout width of the wrapper. @default 'full' */
|
|
1204
|
+
this.width = input('full');
|
|
1205
|
+
/** Name of the leading icon (SVG symbol ID). Leave empty for no icon. */
|
|
1206
|
+
this.leadingIcon = input('');
|
|
1207
|
+
/** Name of the trailing icon (SVG symbol ID). Ignored when `type="password"`. */
|
|
1208
|
+
this.trailingIcon = input('');
|
|
1209
|
+
/**
|
|
1210
|
+
* When `true`, a slot for custom trailing content is enabled
|
|
1211
|
+
* (projects `[trailingSlot]` content). Overrides `trailingIcon`.
|
|
1212
|
+
* @default false
|
|
1213
|
+
*/
|
|
1214
|
+
this.hasTrailingSlot = input(false);
|
|
1215
|
+
/** Marks the field as required. Adds `aria-required` and a visual asterisk. @default false */
|
|
1216
|
+
this.required = input(false);
|
|
1217
|
+
/** When `false`, hides the visual asterisk even if `required=true`. @default true */
|
|
1218
|
+
this.showRequiredIndicator = input(true);
|
|
1219
|
+
/** Human-readable error message. Takes precedence over `errors[0]`. */
|
|
1220
|
+
this.errorMessage = input('');
|
|
1221
|
+
/**
|
|
1222
|
+
* Array of error strings. The first item is displayed when `errorMessage` is empty.
|
|
1223
|
+
* Set together with `invalid=true` to trigger the error state.
|
|
1224
|
+
*/
|
|
1225
|
+
this.errors = input([]);
|
|
1226
|
+
/**
|
|
1227
|
+
* Stable `id` used to link the `<label>` with the `<input>`.
|
|
1228
|
+
* A random suffix is generated by default.
|
|
1229
|
+
*/
|
|
1230
|
+
this.inputId = input(`ku-text-input-${Math.random().toString(36).slice(2, 8)}`);
|
|
1231
|
+
/** Disables the input. @default false */
|
|
1232
|
+
this.disabled = input(false);
|
|
1233
|
+
/**
|
|
1234
|
+
* Forces the error visual state regardless of the `touched` model.
|
|
1235
|
+
* Useful for external form validation. @default false
|
|
1236
|
+
*/
|
|
1237
|
+
this.invalid = input(false);
|
|
1238
|
+
/** Current text value. Use `[(value)]` for two-way binding. */
|
|
1239
|
+
this.value = model('');
|
|
1240
|
+
/** Whether the control has been interacted with. Use `[(touched)]` for two-way binding. */
|
|
1241
|
+
this.touched = model(false);
|
|
1242
|
+
this.errorId = computed(() => `${this.inputId()}-error`);
|
|
1243
|
+
this.isPasswordType = computed(() => this.type() === 'password');
|
|
1244
|
+
this.resolvedType = computed(() => {
|
|
1245
|
+
if (this.isPasswordType()) {
|
|
1246
|
+
return this.isPasswordVisible() ? 'text' : 'password';
|
|
1247
|
+
}
|
|
1248
|
+
return this.type();
|
|
1249
|
+
});
|
|
1250
|
+
this.widthClass = computed(() => {
|
|
1251
|
+
const map = {
|
|
1252
|
+
full: 'w-full',
|
|
1253
|
+
half: 'w-1/2',
|
|
1254
|
+
auto: 'w-auto',
|
|
1255
|
+
};
|
|
1256
|
+
return map[this.width()];
|
|
1257
|
+
});
|
|
1258
|
+
this.hasLeading = computed(() => this.leadingIcon().length > 0);
|
|
1259
|
+
this.hasTrailing = computed(() => this.trailingIcon().length > 0 && !this.isPasswordType());
|
|
1260
|
+
this.showError = computed(() => this.touched() &&
|
|
1261
|
+
(this.invalid() || this.errorMessage().length > 0 || this.errors().length > 0));
|
|
1262
|
+
this.isDateEmpty = computed(() => this.type() === 'date' && !this.value());
|
|
1263
|
+
this.inputClasses = computed(() => {
|
|
1264
|
+
const textColor = this.isDateEmpty()
|
|
1265
|
+
? 'text-ku-gray-text'
|
|
1266
|
+
: 'text-ku-primary-text';
|
|
1267
|
+
const paddingLeft = this.hasLeading() ? 'pl-9' : 'pl-4';
|
|
1268
|
+
const paddingRight = this.hasTrailingSlot()
|
|
1269
|
+
? 'pr-24'
|
|
1270
|
+
: this.hasTrailing() || this.isPasswordType()
|
|
1271
|
+
? 'pr-10'
|
|
1272
|
+
: 'pr-4';
|
|
1273
|
+
const borderColor = this.showError()
|
|
1274
|
+
? 'border-ku-error-primary'
|
|
1275
|
+
: 'border-ku-secondary-border focus-visible:border-ku-action-primary';
|
|
1276
|
+
const cursor = this.type() === 'date' ? 'cursor-pointer' : '';
|
|
1277
|
+
return `${textColor} ${paddingLeft} ${paddingRight} ${borderColor} ${cursor}`;
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
onInput(event) {
|
|
1281
|
+
const target = event.target;
|
|
1282
|
+
this.value.set(target.value);
|
|
1283
|
+
}
|
|
1284
|
+
onBlur() {
|
|
1285
|
+
this.touched.set(true);
|
|
1286
|
+
}
|
|
1287
|
+
openDatePicker() {
|
|
1288
|
+
if (this.type() === 'date') {
|
|
1289
|
+
try {
|
|
1290
|
+
this.inputRef()?.nativeElement.showPicker();
|
|
1291
|
+
}
|
|
1292
|
+
catch { /* showPicker not supported in all browsers */ }
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
togglePasswordVisibility() {
|
|
1296
|
+
this.isPasswordVisible.update(v => !v);
|
|
1297
|
+
}
|
|
1298
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SignalTextInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1299
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: SignalTextInputComponent, isStandalone: true, selector: "keepui-signal-text-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, leadingIcon: { classPropertyName: "leadingIcon", publicName: "leadingIcon", isSignal: true, isRequired: false, transformFunction: null }, trailingIcon: { classPropertyName: "trailingIcon", publicName: "trailingIcon", isSignal: true, isRequired: false, transformFunction: null }, hasTrailingSlot: { classPropertyName: "hasTrailingSlot", publicName: "hasTrailingSlot", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, showRequiredIndicator: { classPropertyName: "showRequiredIndicator", publicName: "showRequiredIndicator", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { classAttribute: "block" }, providers: [{ provide: TRANSLOCO_SCOPE, useValue: 'keepui' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1300
|
+
<div class="flex flex-col gap-1" [class]="widthClass()">
|
|
1301
|
+
|
|
1302
|
+
@if (label()) {
|
|
1303
|
+
<label [for]="inputId()" class="text-sm text-ku-gray-text">
|
|
1304
|
+
{{ label() }}
|
|
1305
|
+
@if (required() && showRequiredIndicator()) {
|
|
1306
|
+
<span class="text-ku-error-primary" aria-hidden="true"> *</span>
|
|
1307
|
+
}
|
|
1308
|
+
</label>
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
<div class="relative flex items-center">
|
|
1312
|
+
|
|
1313
|
+
@if (hasLeading()) {
|
|
1314
|
+
<span
|
|
1315
|
+
class="absolute left-3 flex items-center pointer-events-none text-ku-gray-text"
|
|
1316
|
+
aria-hidden="true"
|
|
1317
|
+
>
|
|
1318
|
+
<keepui-icon [name]="leadingIcon()" [size]="18" />
|
|
1319
|
+
</span>
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
<input
|
|
1323
|
+
#inputRef
|
|
1324
|
+
[id]="inputId()"
|
|
1325
|
+
[type]="resolvedType()"
|
|
1326
|
+
[value]="value()"
|
|
1327
|
+
[placeholder]="placeholder()"
|
|
1328
|
+
[disabled]="disabled()"
|
|
1329
|
+
[attr.required]="required() ? true : null"
|
|
1330
|
+
[attr.aria-required]="required() ? true : null"
|
|
1331
|
+
[attr.aria-invalid]="showError() ? true : null"
|
|
1332
|
+
[attr.aria-describedby]="showError() ? errorId() : null"
|
|
1333
|
+
class="bg-ku-primary border rounded-xl py-2.5 text-sm text-ku-primary-text
|
|
1334
|
+
placeholder:text-ku-gray-text outline-none transition-colors w-full
|
|
1335
|
+
disabled:opacity-50 disabled:cursor-not-allowed min-h-[2.75rem]
|
|
1336
|
+
dark:[color-scheme:dark]"
|
|
1337
|
+
[class]="inputClasses()"
|
|
1338
|
+
(click)="openDatePicker()"
|
|
1339
|
+
(input)="onInput($event)"
|
|
1340
|
+
(blur)="onBlur()"
|
|
1341
|
+
/>
|
|
1342
|
+
|
|
1343
|
+
@if (hasTrailingSlot()) {
|
|
1344
|
+
<div class="absolute right-1 flex items-center">
|
|
1345
|
+
<ng-content select="[trailingSlot]" />
|
|
1346
|
+
</div>
|
|
1347
|
+
} @else if (isPasswordType()) {
|
|
1348
|
+
<button
|
|
1349
|
+
type="button"
|
|
1350
|
+
(click)="togglePasswordVisibility()"
|
|
1351
|
+
class="absolute right-3 flex items-center justify-center text-ku-gray-text
|
|
1352
|
+
hover:text-ku-primary-text transition-colors cursor-pointer
|
|
1353
|
+
focus-visible:outline-none focus-visible:ring-2
|
|
1354
|
+
focus-visible:ring-ku-action-primary rounded min-h-[2.75rem] min-w-[2.75rem]"
|
|
1355
|
+
[attr.aria-label]="(isPasswordVisible()
|
|
1356
|
+
? keys.HIDE_PASSWORD
|
|
1357
|
+
: keys.SHOW_PASSWORD) | transloco"
|
|
1358
|
+
>
|
|
1359
|
+
<keepui-icon
|
|
1360
|
+
[name]="isPasswordVisible() ? 'eye-off-icon' : 'eye-icon'"
|
|
1361
|
+
[size]="18"
|
|
1362
|
+
aria-hidden="true"
|
|
1363
|
+
/>
|
|
1364
|
+
</button>
|
|
1365
|
+
} @else if (hasTrailing()) {
|
|
1366
|
+
<span
|
|
1367
|
+
class="absolute right-3 flex items-center pointer-events-none text-ku-gray-text"
|
|
1368
|
+
aria-hidden="true"
|
|
1369
|
+
>
|
|
1370
|
+
<keepui-icon [name]="trailingIcon()" [size]="18" />
|
|
1371
|
+
</span>
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
</div>
|
|
1375
|
+
|
|
1376
|
+
@if (showError()) {
|
|
1377
|
+
<span
|
|
1378
|
+
[id]="errorId()"
|
|
1379
|
+
class="text-sm text-ku-error-primary"
|
|
1380
|
+
role="alert"
|
|
1381
|
+
>
|
|
1382
|
+
@if (errorMessage()) {
|
|
1383
|
+
{{ errorMessage() }}
|
|
1384
|
+
} @else if (errors().length > 0) {
|
|
1385
|
+
{{ errors()[0] }}
|
|
1386
|
+
}
|
|
1387
|
+
</span>
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
</div>
|
|
1391
|
+
`, isInline: true, dependencies: [{ kind: "component", type: IconComponent, selector: "keepui-icon", inputs: ["name", "size", "viewBox", "ariaLabel"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1392
|
+
}
|
|
1393
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SignalTextInputComponent, decorators: [{
|
|
1394
|
+
type: Component,
|
|
1395
|
+
args: [{
|
|
1396
|
+
selector: 'keepui-signal-text-input',
|
|
1397
|
+
standalone: true,
|
|
1398
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1399
|
+
imports: [IconComponent, TranslocoPipe],
|
|
1400
|
+
providers: [{ provide: TRANSLOCO_SCOPE, useValue: 'keepui' }],
|
|
1401
|
+
host: { class: 'block' },
|
|
1402
|
+
template: `
|
|
1403
|
+
<div class="flex flex-col gap-1" [class]="widthClass()">
|
|
1404
|
+
|
|
1405
|
+
@if (label()) {
|
|
1406
|
+
<label [for]="inputId()" class="text-sm text-ku-gray-text">
|
|
1407
|
+
{{ label() }}
|
|
1408
|
+
@if (required() && showRequiredIndicator()) {
|
|
1409
|
+
<span class="text-ku-error-primary" aria-hidden="true"> *</span>
|
|
1410
|
+
}
|
|
1411
|
+
</label>
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
<div class="relative flex items-center">
|
|
1415
|
+
|
|
1416
|
+
@if (hasLeading()) {
|
|
1417
|
+
<span
|
|
1418
|
+
class="absolute left-3 flex items-center pointer-events-none text-ku-gray-text"
|
|
1419
|
+
aria-hidden="true"
|
|
1420
|
+
>
|
|
1421
|
+
<keepui-icon [name]="leadingIcon()" [size]="18" />
|
|
1422
|
+
</span>
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
<input
|
|
1426
|
+
#inputRef
|
|
1427
|
+
[id]="inputId()"
|
|
1428
|
+
[type]="resolvedType()"
|
|
1429
|
+
[value]="value()"
|
|
1430
|
+
[placeholder]="placeholder()"
|
|
1431
|
+
[disabled]="disabled()"
|
|
1432
|
+
[attr.required]="required() ? true : null"
|
|
1433
|
+
[attr.aria-required]="required() ? true : null"
|
|
1434
|
+
[attr.aria-invalid]="showError() ? true : null"
|
|
1435
|
+
[attr.aria-describedby]="showError() ? errorId() : null"
|
|
1436
|
+
class="bg-ku-primary border rounded-xl py-2.5 text-sm text-ku-primary-text
|
|
1437
|
+
placeholder:text-ku-gray-text outline-none transition-colors w-full
|
|
1438
|
+
disabled:opacity-50 disabled:cursor-not-allowed min-h-[2.75rem]
|
|
1439
|
+
dark:[color-scheme:dark]"
|
|
1440
|
+
[class]="inputClasses()"
|
|
1441
|
+
(click)="openDatePicker()"
|
|
1442
|
+
(input)="onInput($event)"
|
|
1443
|
+
(blur)="onBlur()"
|
|
1444
|
+
/>
|
|
1445
|
+
|
|
1446
|
+
@if (hasTrailingSlot()) {
|
|
1447
|
+
<div class="absolute right-1 flex items-center">
|
|
1448
|
+
<ng-content select="[trailingSlot]" />
|
|
1449
|
+
</div>
|
|
1450
|
+
} @else if (isPasswordType()) {
|
|
1451
|
+
<button
|
|
1452
|
+
type="button"
|
|
1453
|
+
(click)="togglePasswordVisibility()"
|
|
1454
|
+
class="absolute right-3 flex items-center justify-center text-ku-gray-text
|
|
1455
|
+
hover:text-ku-primary-text transition-colors cursor-pointer
|
|
1456
|
+
focus-visible:outline-none focus-visible:ring-2
|
|
1457
|
+
focus-visible:ring-ku-action-primary rounded min-h-[2.75rem] min-w-[2.75rem]"
|
|
1458
|
+
[attr.aria-label]="(isPasswordVisible()
|
|
1459
|
+
? keys.HIDE_PASSWORD
|
|
1460
|
+
: keys.SHOW_PASSWORD) | transloco"
|
|
1461
|
+
>
|
|
1462
|
+
<keepui-icon
|
|
1463
|
+
[name]="isPasswordVisible() ? 'eye-off-icon' : 'eye-icon'"
|
|
1464
|
+
[size]="18"
|
|
1465
|
+
aria-hidden="true"
|
|
1466
|
+
/>
|
|
1467
|
+
</button>
|
|
1468
|
+
} @else if (hasTrailing()) {
|
|
1469
|
+
<span
|
|
1470
|
+
class="absolute right-3 flex items-center pointer-events-none text-ku-gray-text"
|
|
1471
|
+
aria-hidden="true"
|
|
1472
|
+
>
|
|
1473
|
+
<keepui-icon [name]="trailingIcon()" [size]="18" />
|
|
1474
|
+
</span>
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
</div>
|
|
1478
|
+
|
|
1479
|
+
@if (showError()) {
|
|
1480
|
+
<span
|
|
1481
|
+
[id]="errorId()"
|
|
1482
|
+
class="text-sm text-ku-error-primary"
|
|
1483
|
+
role="alert"
|
|
1484
|
+
>
|
|
1485
|
+
@if (errorMessage()) {
|
|
1486
|
+
{{ errorMessage() }}
|
|
1487
|
+
} @else if (errors().length > 0) {
|
|
1488
|
+
{{ errors()[0] }}
|
|
1489
|
+
}
|
|
1490
|
+
</span>
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
</div>
|
|
1494
|
+
`,
|
|
1495
|
+
}]
|
|
1496
|
+
}] });
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* Signal-based accessible textarea component with character counter,
|
|
1500
|
+
* resize control, and integrated error display.
|
|
1501
|
+
*
|
|
1502
|
+
* The `value` and `touched` properties are `model()` signals so the component
|
|
1503
|
+
* integrates seamlessly with Angular signal-based forms.
|
|
1504
|
+
*
|
|
1505
|
+
* ```html
|
|
1506
|
+
* <keepui-signal-textarea
|
|
1507
|
+
* label="Descripción"
|
|
1508
|
+
* placeholder="Escribe aquí…"
|
|
1509
|
+
* [rows]="5"
|
|
1510
|
+
* [maxLength]="500"
|
|
1511
|
+
* [(value)]="description"
|
|
1512
|
+
* />
|
|
1513
|
+
* ```
|
|
1514
|
+
*/
|
|
1515
|
+
class SignalTextareaComponent {
|
|
1516
|
+
constructor() {
|
|
1517
|
+
/** Optional label text rendered above the textarea. */
|
|
1518
|
+
this.label = input('');
|
|
1519
|
+
/** Placeholder passed to the underlying `<textarea>`. */
|
|
1520
|
+
this.placeholder = input('');
|
|
1521
|
+
/** Number of visible text rows. @default 4 */
|
|
1522
|
+
this.rows = input(4);
|
|
1523
|
+
/** Layout width of the wrapper. @default 'full' */
|
|
1524
|
+
this.width = input('full');
|
|
1525
|
+
/** CSS `resize` behaviour. @default 'none' */
|
|
1526
|
+
this.resize = input('none');
|
|
1527
|
+
/** Marks the field as required. @default false */
|
|
1528
|
+
this.required = input(false);
|
|
1529
|
+
/** Human-readable error message. Takes precedence over `errors[0]`. */
|
|
1530
|
+
this.errorMessage = input('');
|
|
1531
|
+
/**
|
|
1532
|
+
* Array of error strings. The first item is displayed when `errorMessage` is empty.
|
|
1533
|
+
* Set together with `invalid=true` to trigger the error state.
|
|
1534
|
+
*/
|
|
1535
|
+
this.errors = input([]);
|
|
1536
|
+
/**
|
|
1537
|
+
* Stable `id` used to link the `<label>` with the `<textarea>`.
|
|
1538
|
+
* A random suffix is generated by default.
|
|
1539
|
+
*/
|
|
1540
|
+
this.textareaId = input(`ku-textarea-${Math.random().toString(36).slice(2, 8)}`);
|
|
1541
|
+
/** Disables the textarea. @default false */
|
|
1542
|
+
this.disabled = input(false);
|
|
1543
|
+
/** Maximum number of characters allowed. Shows a character counter when set. */
|
|
1544
|
+
this.maxLength = input(undefined);
|
|
1545
|
+
/**
|
|
1546
|
+
* Forces the error visual state regardless of the `touched` model.
|
|
1547
|
+
* Useful for external form validation. @default false
|
|
1548
|
+
*/
|
|
1549
|
+
this.invalid = input(false);
|
|
1550
|
+
/** Current text value. Use `[(value)]` for two-way binding. */
|
|
1551
|
+
this.value = model('');
|
|
1552
|
+
/** Whether the control has been interacted with. Use `[(touched)]` for two-way binding. */
|
|
1553
|
+
this.touched = model(false);
|
|
1554
|
+
this.errorId = computed(() => `${this.textareaId()}-error`);
|
|
1555
|
+
this.charCountId = computed(() => `${this.textareaId()}-count`);
|
|
1556
|
+
this.charCount = computed(() => this.value().length);
|
|
1557
|
+
this.showError = computed(() => this.touched() &&
|
|
1558
|
+
(this.invalid() || this.errorMessage().length > 0 || this.errors().length > 0));
|
|
1559
|
+
this.widthClass = computed(() => {
|
|
1560
|
+
const map = {
|
|
1561
|
+
full: 'w-full',
|
|
1562
|
+
half: 'w-1/2',
|
|
1563
|
+
auto: 'w-auto',
|
|
1564
|
+
};
|
|
1565
|
+
return map[this.width()];
|
|
1566
|
+
});
|
|
1567
|
+
this.resizeClass = computed(() => {
|
|
1568
|
+
const map = {
|
|
1569
|
+
none: 'resize-none',
|
|
1570
|
+
vertical: 'resize-y',
|
|
1571
|
+
horizontal: 'resize-x',
|
|
1572
|
+
both: 'resize',
|
|
1573
|
+
};
|
|
1574
|
+
return map[this.resize()];
|
|
1575
|
+
});
|
|
1576
|
+
this.textareaClasses = computed(() => {
|
|
1577
|
+
const borderColor = this.showError()
|
|
1578
|
+
? 'border-ku-error-primary'
|
|
1579
|
+
: 'border-ku-secondary-border focus-visible:border-ku-action-primary';
|
|
1580
|
+
return `${this.resizeClass()} ${borderColor}`;
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
onInput(event) {
|
|
1584
|
+
const target = event.target;
|
|
1585
|
+
this.value.set(target.value);
|
|
1586
|
+
}
|
|
1587
|
+
onBlur() {
|
|
1588
|
+
this.touched.set(true);
|
|
1589
|
+
}
|
|
1590
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SignalTextareaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1591
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: SignalTextareaComponent, isStandalone: true, selector: "keepui-signal-textarea", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, resize: { classPropertyName: "resize", publicName: "resize", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, textareaId: { classPropertyName: "textareaId", publicName: "textareaId", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
1592
|
+
<div class="flex flex-col gap-1" [class]="widthClass()">
|
|
1593
|
+
|
|
1594
|
+
@if (label()) {
|
|
1595
|
+
<label [for]="textareaId()" class="text-sm text-ku-gray-text">
|
|
1596
|
+
{{ label() }}
|
|
1597
|
+
@if (required()) {
|
|
1598
|
+
<span class="text-ku-error-primary" aria-hidden="true"> *</span>
|
|
1599
|
+
}
|
|
1600
|
+
</label>
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
<textarea
|
|
1604
|
+
[id]="textareaId()"
|
|
1605
|
+
[value]="value()"
|
|
1606
|
+
[placeholder]="placeholder()"
|
|
1607
|
+
[rows]="rows()"
|
|
1608
|
+
[disabled]="disabled()"
|
|
1609
|
+
[attr.maxlength]="maxLength() ?? null"
|
|
1610
|
+
[attr.required]="required() ? true : null"
|
|
1611
|
+
[attr.aria-required]="required() ? true : null"
|
|
1612
|
+
[attr.aria-invalid]="showError() ? true : null"
|
|
1613
|
+
[attr.aria-describedby]="showError() ? errorId() : (maxLength() ? charCountId() : null)"
|
|
1614
|
+
class="bg-ku-primary border rounded-xl p-3 text-sm text-ku-primary-text
|
|
1615
|
+
placeholder:text-ku-gray-text outline-none transition-colors w-full
|
|
1616
|
+
disabled:opacity-50 disabled:cursor-not-allowed"
|
|
1617
|
+
[class]="textareaClasses()"
|
|
1618
|
+
(input)="onInput($event)"
|
|
1619
|
+
(blur)="onBlur()"
|
|
1620
|
+
></textarea>
|
|
1621
|
+
|
|
1622
|
+
<div class="flex items-start justify-between gap-2">
|
|
1623
|
+
|
|
1624
|
+
<span
|
|
1625
|
+
[id]="errorId()"
|
|
1626
|
+
class="text-sm text-ku-error-primary"
|
|
1627
|
+
role="alert"
|
|
1628
|
+
>
|
|
1629
|
+
@if (showError()) {
|
|
1630
|
+
@if (errorMessage()) {
|
|
1631
|
+
{{ errorMessage() }}
|
|
1632
|
+
} @else if (errors().length > 0) {
|
|
1633
|
+
{{ errors()[0] }}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
</span>
|
|
1637
|
+
|
|
1638
|
+
@if (maxLength()) {
|
|
1639
|
+
<span
|
|
1640
|
+
[id]="charCountId()"
|
|
1641
|
+
class="text-xs text-ku-gray-text ml-auto shrink-0"
|
|
1642
|
+
aria-live="polite"
|
|
1643
|
+
aria-atomic="true"
|
|
1644
|
+
>
|
|
1645
|
+
{{ charCount() }} / {{ maxLength() }}
|
|
1646
|
+
</span>
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
</div>
|
|
1650
|
+
|
|
1651
|
+
</div>
|
|
1652
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1653
|
+
}
|
|
1654
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SignalTextareaComponent, decorators: [{
|
|
1655
|
+
type: Component,
|
|
1656
|
+
args: [{
|
|
1657
|
+
selector: 'keepui-signal-textarea',
|
|
1658
|
+
standalone: true,
|
|
1659
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1660
|
+
host: { class: 'block' },
|
|
1661
|
+
template: `
|
|
1662
|
+
<div class="flex flex-col gap-1" [class]="widthClass()">
|
|
1663
|
+
|
|
1664
|
+
@if (label()) {
|
|
1665
|
+
<label [for]="textareaId()" class="text-sm text-ku-gray-text">
|
|
1666
|
+
{{ label() }}
|
|
1667
|
+
@if (required()) {
|
|
1668
|
+
<span class="text-ku-error-primary" aria-hidden="true"> *</span>
|
|
1669
|
+
}
|
|
1670
|
+
</label>
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
<textarea
|
|
1674
|
+
[id]="textareaId()"
|
|
1675
|
+
[value]="value()"
|
|
1676
|
+
[placeholder]="placeholder()"
|
|
1677
|
+
[rows]="rows()"
|
|
1678
|
+
[disabled]="disabled()"
|
|
1679
|
+
[attr.maxlength]="maxLength() ?? null"
|
|
1680
|
+
[attr.required]="required() ? true : null"
|
|
1681
|
+
[attr.aria-required]="required() ? true : null"
|
|
1682
|
+
[attr.aria-invalid]="showError() ? true : null"
|
|
1683
|
+
[attr.aria-describedby]="showError() ? errorId() : (maxLength() ? charCountId() : null)"
|
|
1684
|
+
class="bg-ku-primary border rounded-xl p-3 text-sm text-ku-primary-text
|
|
1685
|
+
placeholder:text-ku-gray-text outline-none transition-colors w-full
|
|
1686
|
+
disabled:opacity-50 disabled:cursor-not-allowed"
|
|
1687
|
+
[class]="textareaClasses()"
|
|
1688
|
+
(input)="onInput($event)"
|
|
1689
|
+
(blur)="onBlur()"
|
|
1690
|
+
></textarea>
|
|
1691
|
+
|
|
1692
|
+
<div class="flex items-start justify-between gap-2">
|
|
1693
|
+
|
|
1694
|
+
<span
|
|
1695
|
+
[id]="errorId()"
|
|
1696
|
+
class="text-sm text-ku-error-primary"
|
|
1697
|
+
role="alert"
|
|
1698
|
+
>
|
|
1699
|
+
@if (showError()) {
|
|
1700
|
+
@if (errorMessage()) {
|
|
1701
|
+
{{ errorMessage() }}
|
|
1702
|
+
} @else if (errors().length > 0) {
|
|
1703
|
+
{{ errors()[0] }}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
</span>
|
|
1707
|
+
|
|
1708
|
+
@if (maxLength()) {
|
|
1709
|
+
<span
|
|
1710
|
+
[id]="charCountId()"
|
|
1711
|
+
class="text-xs text-ku-gray-text ml-auto shrink-0"
|
|
1712
|
+
aria-live="polite"
|
|
1713
|
+
aria-atomic="true"
|
|
1714
|
+
>
|
|
1715
|
+
{{ charCount() }} / {{ maxLength() }}
|
|
1716
|
+
</span>
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
</div>
|
|
1720
|
+
|
|
1721
|
+
</div>
|
|
1722
|
+
`,
|
|
1723
|
+
}]
|
|
1724
|
+
}] });
|
|
1725
|
+
|
|
511
1726
|
/**
|
|
512
1727
|
* Registers KeepUI core providers for a **web** Angular application.
|
|
513
1728
|
*
|
|
@@ -543,6 +1758,10 @@ const EN = {
|
|
|
543
1758
|
previewAlt: 'Selected image preview',
|
|
544
1759
|
errorUnexpected: 'An unexpected error occurred',
|
|
545
1760
|
},
|
|
1761
|
+
signalTextInput: {
|
|
1762
|
+
showPassword: 'Show password',
|
|
1763
|
+
hidePassword: 'Hide password',
|
|
1764
|
+
},
|
|
546
1765
|
};
|
|
547
1766
|
const ES = {
|
|
548
1767
|
imagePreview: {
|
|
@@ -551,6 +1770,10 @@ const ES = {
|
|
|
551
1770
|
previewAlt: 'Vista previa de imagen seleccionada',
|
|
552
1771
|
errorUnexpected: 'Ha ocurrido un error inesperado',
|
|
553
1772
|
},
|
|
1773
|
+
signalTextInput: {
|
|
1774
|
+
showPassword: 'Mostrar contraseña',
|
|
1775
|
+
hidePassword: 'Ocultar contraseña',
|
|
1776
|
+
},
|
|
554
1777
|
};
|
|
555
1778
|
const DE = {
|
|
556
1779
|
imagePreview: {
|
|
@@ -559,6 +1782,10 @@ const DE = {
|
|
|
559
1782
|
previewAlt: 'Vorschau des ausgewählten Bildes',
|
|
560
1783
|
errorUnexpected: 'Ein unerwarteter Fehler ist aufgetreten',
|
|
561
1784
|
},
|
|
1785
|
+
signalTextInput: {
|
|
1786
|
+
showPassword: 'Passwort anzeigen',
|
|
1787
|
+
hidePassword: 'Passwort verbergen',
|
|
1788
|
+
},
|
|
562
1789
|
};
|
|
563
1790
|
/** Map from locale code to its translation object. */
|
|
564
1791
|
const KEEPUI_TRANSLATIONS = {
|
|
@@ -723,5 +1950,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
|
|
|
723
1950
|
* Generated bundle index. Do not edit.
|
|
724
1951
|
*/
|
|
725
1952
|
|
|
726
|
-
export { ButtonComponent, CardComponent, FILE_PORT, ImagePreviewComponent, KEEPUI_AVAILABLE_LANGUAGES, KEEPUI_TRANSLATIONS, KEEPUI_TRANSLATION_KEYS, KeepUiLanguageService, MockFileService, WebFileService, provideKeepUi, provideKeepUiI18n };
|
|
1953
|
+
export { ButtonComponent, CardComponent, FILE_PORT, IconActionButtonComponent, IconComponent, ImagePreviewComponent, KEEPUI_AVAILABLE_LANGUAGES, KEEPUI_TRANSLATIONS, KEEPUI_TRANSLATION_KEYS, KeepUiLanguageService, MockFileService, SignalDropdownComponent, SignalTextInputComponent, SignalTextareaComponent, WebFileService, provideKeepUi, provideKeepUiI18n };
|
|
727
1954
|
//# sourceMappingURL=keepui-ui.mjs.map
|