@egose/shadcn-theme-ng 0.6.0 → 0.6.1
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 +119 -1
- package/autocomplete/fesm2022/autocomplete.mjs +14 -4
- package/autocomplete/types/autocomplete.d.ts +4 -1
- package/checkbox/README.md +6 -2
- package/checkbox/fesm2022/checkbox.mjs +11 -7
- package/checkbox/types/checkbox.d.ts +6 -4
- package/combobox/fesm2022/combobox.mjs +13 -7
- package/combobox/types/combobox.d.ts +8 -2
- package/data-table/README.md +10 -0
- package/data-table/fesm2022/data-table.mjs +48 -12
- package/data-table/types/data-table.d.ts +16 -4
- package/date-picker/README.md +41 -9
- package/date-picker/fesm2022/date-picker.mjs +265 -59
- package/date-picker/types/date-picker.d.ts +58 -27
- package/form-autocomplete/README.md +4 -2
- package/form-autocomplete/fesm2022/form-autocomplete.mjs +19 -6
- package/form-autocomplete/types/form-autocomplete.d.ts +9 -3
- package/form-combobox/README.md +4 -2
- package/form-combobox/fesm2022/form-combobox.mjs +23 -6
- package/form-combobox/types/form-combobox.d.ts +10 -3
- package/form-input-otp/README.md +7 -5
- package/form-input-otp/fesm2022/form-input-otp.mjs +20 -8
- package/form-input-otp/types/form-input-otp.d.ts +9 -3
- package/form-searchable-multiselect/README.md +15 -1
- package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +15 -3
- package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +9 -1
- package/form-slider/README.md +3 -1
- package/form-slider/fesm2022/form-slider.mjs +19 -6
- package/form-slider/types/form-slider.d.ts +9 -3
- package/form-toggle/README.md +22 -18
- package/form-toggle/fesm2022/form-toggle.mjs +22 -9
- package/form-toggle/types/form-toggle.d.ts +6 -2
- package/input-otp/README.md +33 -9
- package/input-otp/fesm2022/input-otp.mjs +127 -11
- package/input-otp/types/input-otp.d.ts +24 -4
- package/package.json +1 -1
- package/pagination/README.md +21 -12
- package/pagination/fesm2022/pagination.mjs +73 -65
- package/pagination/types/pagination.d.ts +21 -8
- package/phone-input/README.md +16 -0
- package/phone-input/fesm2022/phone-input.mjs +41 -3
- package/phone-input/types/phone-input.d.ts +5 -2
- package/searchable-multiselect/README.md +62 -40
- package/searchable-multiselect/fesm2022/searchable-multiselect.mjs +59 -32
- package/searchable-multiselect/types/searchable-multiselect.d.ts +23 -6
- package/slider/fesm2022/slider.mjs +9 -5
- package/slider/types/slider.d.ts +6 -2
- package/stepper/README.md +39 -19
- package/stepper/fesm2022/stepper.mjs +64 -12
- package/stepper/types/stepper.d.ts +12 -1
|
@@ -53,6 +53,8 @@ class EgFormToggle {
|
|
|
53
53
|
...(ngDevMode ? [{ debugName: "autoError" }] : /* istanbul ignore next */ []));
|
|
54
54
|
hint = input(undefined, /* @ts-ignore */
|
|
55
55
|
...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
56
|
+
/** Consumer-owned description IDs, merged with the displayed error or hint on the button. */
|
|
57
|
+
ariaDescribedBy = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedBy" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
|
|
56
58
|
id = input(undefined, /* @ts-ignore */
|
|
57
59
|
...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
|
|
58
60
|
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
@@ -78,12 +80,14 @@ class EgFormToggle {
|
|
|
78
80
|
...(ngDevMode ? [{ debugName: "_status" }] : /* istanbul ignore next */ []));
|
|
79
81
|
_submitted = signal(false, /* @ts-ignore */
|
|
80
82
|
...(ngDevMode ? [{ debugName: "_submitted" }] : /* istanbul ignore next */ []));
|
|
83
|
+
_interacted = signal(false, /* @ts-ignore */
|
|
84
|
+
...(ngDevMode ? [{ debugName: "_interacted" }] : /* istanbul ignore next */ []));
|
|
81
85
|
/**
|
|
82
86
|
* Displayed message: explicit `error()` wins; otherwise (when `autoError()`)
|
|
83
87
|
* auto-resolved from the control's `ValidationErrors`. Plain method (not a
|
|
84
88
|
* computed): `ValidationErrors` is not a signal, so the control must be read
|
|
85
89
|
* fresh on every change-detection pass (`showError()` re-evaluates with it
|
|
86
|
-
* via the `_status`/`_submitted` signals).
|
|
90
|
+
* via the `_status`/`_submitted`/`_interacted` signals).
|
|
87
91
|
*/
|
|
88
92
|
resolvedError() {
|
|
89
93
|
const explicit = this.error();
|
|
@@ -96,20 +100,26 @@ class EgFormToggle {
|
|
|
96
100
|
showError = computed(() => {
|
|
97
101
|
this._status();
|
|
98
102
|
this._submitted();
|
|
103
|
+
this._interacted();
|
|
99
104
|
const control = this._controlDir?.control;
|
|
100
|
-
return
|
|
101
|
-
!!control?.errors &&
|
|
102
|
-
(control.touched || control.dirty || !!this._formGroupDirective?.submitted));
|
|
105
|
+
return !!this.resolvedError() && !!control?.errors && (this._interacted() || this._submitted());
|
|
103
106
|
}, /* @ts-ignore */
|
|
104
107
|
...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
|
|
105
108
|
showHint = computed(() => !this.showError() && !!this.hint(), /* @ts-ignore */
|
|
106
109
|
...(ngDevMode ? [{ debugName: "showHint" }] : /* istanbul ignore next */ []));
|
|
110
|
+
describedBy() {
|
|
111
|
+
const ids = [this.ariaDescribedBy(), this.showError() ? this.errorId() : this.showHint() ? this.hintId() : null];
|
|
112
|
+
return [...new Set(ids.filter(Boolean).join(' ').split(/\s+/).filter(Boolean))].join(' ') || null;
|
|
113
|
+
}
|
|
107
114
|
_onChange;
|
|
108
115
|
_onTouched;
|
|
109
116
|
ngDoCheck() {
|
|
110
117
|
const submitted = !!this._formGroupDirective?.submitted;
|
|
111
118
|
if (this._submitted() !== submitted)
|
|
112
119
|
this._submitted.set(submitted);
|
|
120
|
+
// Keep silent interaction updates (emitEvent: false) visible on the next check too.
|
|
121
|
+
const control = this._controlDir?.control;
|
|
122
|
+
this._interacted.set(!!control && (control.touched || control.dirty));
|
|
113
123
|
}
|
|
114
124
|
ngAfterViewInit() {
|
|
115
125
|
// Resolved here (not earlier): this component IS the value accessor for
|
|
@@ -119,8 +129,11 @@ class EgFormToggle {
|
|
|
119
129
|
this._controlDir = this._injector.get(FormControlName, null, { self: true, optional: true }) ?? null;
|
|
120
130
|
const control = this._controlDir?.control;
|
|
121
131
|
this._status.set(control?.status ?? null);
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
this._interacted.set(!!control && (control.touched || control.dirty));
|
|
133
|
+
// Touch/dirty/reset events must invalidate feedback even when status stays INVALID.
|
|
134
|
+
control?.events.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(() => {
|
|
135
|
+
this._status.set(control.status);
|
|
136
|
+
this._interacted.set(control.touched || control.dirty);
|
|
124
137
|
});
|
|
125
138
|
}
|
|
126
139
|
_handleStateChange(state) {
|
|
@@ -167,7 +180,7 @@ class EgFormToggle {
|
|
|
167
180
|
$hintClass = computed(() => hlm("mt-0", this._shared.hintClass, this._config.hintClass, this.hintClass()), /* @ts-ignore */
|
|
168
181
|
...(ngDevMode ? [{ debugName: "$hintClass" }] : /* istanbul ignore next */ []));
|
|
169
182
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgFormToggle, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
170
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgFormToggle, isStandalone: true, selector: "eg-form-toggle", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, controlId: { classPropertyName: "controlId", publicName: "controlId", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, autoError: { classPropertyName: "autoError", publicName: "autoError", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, toggleClass: { classPropertyName: "toggleClass", publicName: "toggleClass", isSignal: true, isRequired: false, transformFunction: null }, labelClass: { classPropertyName: "labelClass", publicName: "labelClass", isSignal: true, isRequired: false, transformFunction: null }, errorClass: { classPropertyName: "errorClass", publicName: "errorClass", isSignal: true, isRequired: false, transformFunction: null }, hintClass: { classPropertyName: "hintClass", publicName: "hintClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "$userClass()" } }, providers: [EG_FORM_TOGGLE_VALUE_ACCESSOR, provideIcons({ lucideCheck })], ngImport: i0, template: "\n @let lbl = label();\n @let rqrd = required();\n\n <div>\n <div class=\"flex items-center gap-1\">\n <button\n hlmToggle\n type=\"button\"\n [id]=\"effectiveId()\"\n [state]=\"_state() ? 'on' : 'off'\"\n (stateChange)=\"_handleStateChange($event)\"\n [disabled]=\"_disabled()\"\n [variant]=\"variant()\"\n [size]=\"size()\"\n [class]=\"$toggleClass()\"\n >\n @if (_state()) {\n <ng-icon name=\"lucideCheck\" />\n }\n </button>\n @if (lbl) {\n <label hlmLabel [for]=\"effectiveId()\" [class]=\"$labelClass()\"\n >{{ lbl }}\n @if (rqrd) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n </div>\n\n @if (showError()) {\n <hlm-error [id]=\"errorId()\" [class]=\"$errorClass()\">\n {{ resolvedError() }}\n </hlm-error>\n } @else if (showHint()) {\n <hlm-hint [id]=\"hintId()\" [class]=\"$hintClass()\">\n {{ hint() }}\n </hlm-hint>\n }\n </div>\n ", isInline: true, dependencies: [{ kind: "directive", type: HlmToggle, selector: "button[hlmToggle]", inputs: ["variant", "size"] }, { kind: "directive", type: HlmError, selector: "hlm-error", inputs: ["class"] }, { kind: "directive", type: HlmHint, selector: "hlm-hint", inputs: ["class"] }, { kind: "directive", type: HlmLabel, selector: "[hlmLabel]", inputs: ["class"] }, { kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
183
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgFormToggle, isStandalone: true, selector: "eg-form-toggle", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, controlId: { classPropertyName: "controlId", publicName: "controlId", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, autoError: { classPropertyName: "autoError", publicName: "autoError", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, toggleClass: { classPropertyName: "toggleClass", publicName: "toggleClass", isSignal: true, isRequired: false, transformFunction: null }, labelClass: { classPropertyName: "labelClass", publicName: "labelClass", isSignal: true, isRequired: false, transformFunction: null }, errorClass: { classPropertyName: "errorClass", publicName: "errorClass", isSignal: true, isRequired: false, transformFunction: null }, hintClass: { classPropertyName: "hintClass", publicName: "hintClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "$userClass()" } }, providers: [EG_FORM_TOGGLE_VALUE_ACCESSOR, provideIcons({ lucideCheck })], ngImport: i0, template: "\n @let lbl = label();\n @let rqrd = required();\n\n <div>\n <div class=\"flex items-center gap-1\">\n <button\n hlmToggle\n type=\"button\"\n [id]=\"effectiveId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? 'true' : null\"\n [state]=\"_state() ? 'on' : 'off'\"\n (stateChange)=\"_handleStateChange($event)\"\n [disabled]=\"_disabled()\"\n [variant]=\"variant()\"\n [size]=\"size()\"\n [class]=\"$toggleClass()\"\n >\n @if (_state()) {\n <ng-icon name=\"lucideCheck\" />\n }\n </button>\n @if (lbl) {\n <label hlmLabel [for]=\"effectiveId()\" [class]=\"$labelClass()\"\n >{{ lbl }}\n @if (rqrd) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n </div>\n\n @if (showError()) {\n <hlm-error [id]=\"errorId()\" [class]=\"$errorClass()\">\n {{ resolvedError() }}\n </hlm-error>\n } @else if (showHint()) {\n <hlm-hint [id]=\"hintId()\" [class]=\"$hintClass()\">\n {{ hint() }}\n </hlm-hint>\n }\n </div>\n ", isInline: true, dependencies: [{ kind: "directive", type: HlmToggle, selector: "button[hlmToggle]", inputs: ["variant", "size"] }, { kind: "directive", type: HlmError, selector: "hlm-error", inputs: ["class"] }, { kind: "directive", type: HlmHint, selector: "hlm-hint", inputs: ["class"] }, { kind: "directive", type: HlmLabel, selector: "[hlmLabel]", inputs: ["class"] }, { kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
171
184
|
}
|
|
172
185
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgFormToggle, decorators: [{
|
|
173
186
|
type: Component,
|
|
@@ -180,9 +193,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
180
193
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
181
194
|
imports: [HlmToggle, HlmError, HlmHint, HlmLabel, NgIcon],
|
|
182
195
|
providers: [EG_FORM_TOGGLE_VALUE_ACCESSOR, provideIcons({ lucideCheck })],
|
|
183
|
-
template: "\n @let lbl = label();\n @let rqrd = required();\n\n <div>\n <div class=\"flex items-center gap-1\">\n <button\n hlmToggle\n type=\"button\"\n [id]=\"effectiveId()\"\n [state]=\"_state() ? 'on' : 'off'\"\n (stateChange)=\"_handleStateChange($event)\"\n [disabled]=\"_disabled()\"\n [variant]=\"variant()\"\n [size]=\"size()\"\n [class]=\"$toggleClass()\"\n >\n @if (_state()) {\n <ng-icon name=\"lucideCheck\" />\n }\n </button>\n @if (lbl) {\n <label hlmLabel [for]=\"effectiveId()\" [class]=\"$labelClass()\"\n >{{ lbl }}\n @if (rqrd) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n </div>\n\n @if (showError()) {\n <hlm-error [id]=\"errorId()\" [class]=\"$errorClass()\">\n {{ resolvedError() }}\n </hlm-error>\n } @else if (showHint()) {\n <hlm-hint [id]=\"hintId()\" [class]=\"$hintClass()\">\n {{ hint() }}\n </hlm-hint>\n }\n </div>\n ",
|
|
196
|
+
template: "\n @let lbl = label();\n @let rqrd = required();\n\n <div>\n <div class=\"flex items-center gap-1\">\n <button\n hlmToggle\n type=\"button\"\n [id]=\"effectiveId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? 'true' : null\"\n [state]=\"_state() ? 'on' : 'off'\"\n (stateChange)=\"_handleStateChange($event)\"\n [disabled]=\"_disabled()\"\n [variant]=\"variant()\"\n [size]=\"size()\"\n [class]=\"$toggleClass()\"\n >\n @if (_state()) {\n <ng-icon name=\"lucideCheck\" />\n }\n </button>\n @if (lbl) {\n <label hlmLabel [for]=\"effectiveId()\" [class]=\"$labelClass()\"\n >{{ lbl }}\n @if (rqrd) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n </div>\n\n @if (showError()) {\n <hlm-error [id]=\"errorId()\" [class]=\"$errorClass()\">\n {{ resolvedError() }}\n </hlm-error>\n } @else if (showHint()) {\n <hlm-hint [id]=\"hintId()\" [class]=\"$hintClass()\">\n {{ hint() }}\n </hlm-hint>\n }\n </div>\n ",
|
|
184
197
|
}]
|
|
185
|
-
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], controlId: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlId", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], autoError: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoError", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], toggleClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleClass", required: false }] }], labelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelClass", required: false }] }], errorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorClass", required: false }] }], hintClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "hintClass", required: false }] }] } });
|
|
198
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], controlId: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlId", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], autoError: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoError", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], toggleClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleClass", required: false }] }], labelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelClass", required: false }] }], errorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorClass", required: false }] }], hintClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "hintClass", required: false }] }] } });
|
|
186
199
|
|
|
187
200
|
/*
|
|
188
201
|
* Public API Surface of form-toggle
|
|
@@ -36,6 +36,8 @@ declare class EgFormToggle implements ControlValueAccessor, AfterViewInit, DoChe
|
|
|
36
36
|
*/
|
|
37
37
|
autoError: _angular_core.InputSignal<boolean>;
|
|
38
38
|
hint: _angular_core.InputSignal<string | undefined>;
|
|
39
|
+
/** Consumer-owned description IDs, merged with the displayed error or hint on the button. */
|
|
40
|
+
ariaDescribedBy: _angular_core.InputSignal<string | null>;
|
|
39
41
|
id: _angular_core.InputSignal<string | undefined>;
|
|
40
42
|
disabled: _angular_core.InputSignalWithTransform<boolean, BooleanInput>;
|
|
41
43
|
required: _angular_core.InputSignal<boolean>;
|
|
@@ -49,16 +51,18 @@ declare class EgFormToggle implements ControlValueAccessor, AfterViewInit, DoChe
|
|
|
49
51
|
protected readonly _disabled: _angular_core.Signal<boolean>;
|
|
50
52
|
private readonly _status;
|
|
51
53
|
private readonly _submitted;
|
|
54
|
+
private readonly _interacted;
|
|
52
55
|
/**
|
|
53
56
|
* Displayed message: explicit `error()` wins; otherwise (when `autoError()`)
|
|
54
57
|
* auto-resolved from the control's `ValidationErrors`. Plain method (not a
|
|
55
58
|
* computed): `ValidationErrors` is not a signal, so the control must be read
|
|
56
59
|
* fresh on every change-detection pass (`showError()` re-evaluates with it
|
|
57
|
-
* via the `_status`/`_submitted` signals).
|
|
60
|
+
* via the `_status`/`_submitted`/`_interacted` signals).
|
|
58
61
|
*/
|
|
59
62
|
protected resolvedError(): string | undefined;
|
|
60
63
|
protected readonly showError: _angular_core.Signal<boolean>;
|
|
61
64
|
protected readonly showHint: _angular_core.Signal<boolean>;
|
|
65
|
+
protected describedBy(): string | null;
|
|
62
66
|
protected _onChange?: ChangeFn<boolean>;
|
|
63
67
|
protected _onTouched?: TouchFn;
|
|
64
68
|
ngDoCheck(): void;
|
|
@@ -80,7 +84,7 @@ declare class EgFormToggle implements ControlValueAccessor, AfterViewInit, DoChe
|
|
|
80
84
|
$errorClass: _angular_core.Signal<string>;
|
|
81
85
|
$hintClass: _angular_core.Signal<string>;
|
|
82
86
|
static ɵfac: _angular_core.ɵɵFactoryDeclaration<EgFormToggle, never>;
|
|
83
|
-
static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgFormToggle, "eg-form-toggle", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "controlId": { "alias": "controlId"; "required": false; "isSignal": true; }; "error": { "alias": "error"; "required": false; "isSignal": true; }; "autoError": { "alias": "autoError"; "required": false; "isSignal": true; }; "hint": { "alias": "hint"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "toggleClass": { "alias": "toggleClass"; "required": false; "isSignal": true; }; "labelClass": { "alias": "labelClass"; "required": false; "isSignal": true; }; "errorClass": { "alias": "errorClass"; "required": false; "isSignal": true; }; "hintClass": { "alias": "hintClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
87
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgFormToggle, "eg-form-toggle", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "controlId": { "alias": "controlId"; "required": false; "isSignal": true; }; "error": { "alias": "error"; "required": false; "isSignal": true; }; "autoError": { "alias": "autoError"; "required": false; "isSignal": true; }; "hint": { "alias": "hint"; "required": false; "isSignal": true; }; "ariaDescribedBy": { "alias": "aria-describedby"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "toggleClass": { "alias": "toggleClass"; "required": false; "isSignal": true; }; "labelClass": { "alias": "labelClass"; "required": false; "isSignal": true; }; "errorClass": { "alias": "errorClass"; "required": false; "isSignal": true; }; "hintClass": { "alias": "hintClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
interface EgFormToggleConfig {
|
package/input-otp/README.md
CHANGED
|
@@ -61,7 +61,7 @@ import { HlmInputOtpModule } from '@egose/shadcn-theme-ng/input-otp';
|
|
|
61
61
|
export class MyModule {}
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Exported from `src/public-api.ts`: `HlmInputOtp`, `HlmInputOtpGroup`, `HlmInputOtpSeparator`, `HlmInputOtpSlot`, `HlmInputOtpFakeCaret`, plus `HlmInputOtpImports` and `HlmInputOtpModule`.
|
|
64
|
+
Exported from `src/public-api.ts`: `HlmInputOtp`, `HlmInputOtpControl`, `HlmInputOtpGroup`, `HlmInputOtpSeparator`, `HlmInputOtpSlot`, `HlmInputOtpFakeCaret`, plus `HlmInputOtpImports` and `HlmInputOtpModule`.
|
|
65
65
|
|
|
66
66
|
## Anatomy / Structure
|
|
67
67
|
|
|
@@ -88,6 +88,32 @@ Brain inputs on `brn-input-otp` (from `@spartan-ng/brain/input-otp`): `length` (
|
|
|
88
88
|
|
|
89
89
|
## API reference
|
|
90
90
|
|
|
91
|
+
### `hlm-input-otp` — `HlmInputOtpControl`
|
|
92
|
+
|
|
93
|
+
Use this component for native-input accessibility forwarding. It extends `BrnInputOtp` with
|
|
94
|
+
a declarative input template and retains its inputs, outputs, CVA, paste and focus behavior.
|
|
95
|
+
`HlmInputOtpImports` / `HlmInputOtpModule` include it; a separate brain import is unnecessary.
|
|
96
|
+
Compose it with `HlmInputOtpSlot` (the raw brain slot's private injection token is not supported).
|
|
97
|
+
|
|
98
|
+
- `[aria-describedby]` (`string | null`, default `null`) reaches the native input and merges
|
|
99
|
+
with enclosing Spartan field description IDs, deduplicated. Bind only IDs of mounted elements.
|
|
100
|
+
- `required` (boolean-coerced, default `false`) exposes native `aria-required`. Add Angular
|
|
101
|
+
validators separately; native `aria-invalid` follows the bound form control.
|
|
102
|
+
- `inputId` connects a normal `<label for>` to the one focusable native input.
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<label for="verification">Verification code</label>
|
|
106
|
+
<hlm-input-otp [length]="4" inputId="verification" aria-describedby="verification-help" required>
|
|
107
|
+
<div hlmInputOtpGroup>
|
|
108
|
+
<hlm-input-otp-slot index="0" />
|
|
109
|
+
<hlm-input-otp-slot index="1" />
|
|
110
|
+
<hlm-input-otp-slot index="2" />
|
|
111
|
+
<hlm-input-otp-slot index="3" />
|
|
112
|
+
</div>
|
|
113
|
+
</hlm-input-otp>
|
|
114
|
+
<p id="verification-help">Enter the four-character code.</p>
|
|
115
|
+
```
|
|
116
|
+
|
|
91
117
|
### `brn-input-otp[hlmInputOtp]` — `HlmInputOtp`
|
|
92
118
|
|
|
93
119
|
Pure styling directive (`data-slot="input-otp"`, flex row, dims on `has-disabled`). No inputs/outputs — bind `length`, `value`, `disabled`, `inputId`, … on the `brn-input-otp` element itself.
|
|
@@ -102,7 +128,7 @@ Slot cluster wrapper (`data-slot="input-otp-group"`). No inputs/outputs. Use one
|
|
|
102
128
|
| ------- | -------------------------------------------------- | -------------------------------------------------------- |
|
|
103
129
|
| `index` | `number` (**required**, `numberAttribute` coerced) | Zero-based position of the character this slot displays. |
|
|
104
130
|
|
|
105
|
-
|
|
131
|
+
Under `brn-input-otp`, renders `<brn-input-otp-slot [index]>`; under `hlm-input-otp`, reads the adapter's public context and renders the same character/caret and state attributes declaratively. Active slots get the `ring` highlight.
|
|
106
132
|
|
|
107
133
|
### `hlm-input-otp-separator` — `HlmInputOtpSeparator`
|
|
108
134
|
|
|
@@ -267,17 +293,15 @@ export class LengthsExample {}
|
|
|
267
293
|
|
|
268
294
|
```ts
|
|
269
295
|
import { Component, signal } from '@angular/core';
|
|
270
|
-
import { BrnInputOtp } from '@spartan-ng/brain/input-otp';
|
|
271
296
|
import { HlmInputOtpImports } from '@egose/shadcn-theme-ng/input-otp';
|
|
272
297
|
import { HlmError } from '@egose/shadcn-theme-ng/form-field';
|
|
273
298
|
|
|
274
299
|
@Component({
|
|
275
300
|
standalone: true,
|
|
276
|
-
imports: [
|
|
301
|
+
imports: [HlmInputOtpImports, HlmError],
|
|
277
302
|
template: `
|
|
278
303
|
<label for="otp-verify">Verification code</label>
|
|
279
|
-
<
|
|
280
|
-
hlmInputOtp
|
|
304
|
+
<hlm-input-otp
|
|
281
305
|
[length]="6"
|
|
282
306
|
inputId="otp-verify"
|
|
283
307
|
inputMode="numeric"
|
|
@@ -286,7 +310,7 @@ import { HlmError } from '@egose/shadcn-theme-ng/form-field';
|
|
|
286
310
|
[value]="code()"
|
|
287
311
|
(valueChange)="code.set($event); error.set(null)"
|
|
288
312
|
(completed)="verify()"
|
|
289
|
-
aria-describedby="otp-error"
|
|
313
|
+
[aria-describedby]="error() ? 'otp-error' : null"
|
|
290
314
|
>
|
|
291
315
|
<div hlmInputOtpGroup>
|
|
292
316
|
<hlm-input-otp-slot index="0" />
|
|
@@ -299,7 +323,7 @@ import { HlmError } from '@egose/shadcn-theme-ng/form-field';
|
|
|
299
323
|
<hlm-input-otp-slot index="4" />
|
|
300
324
|
<hlm-input-otp-slot index="5" />
|
|
301
325
|
</div>
|
|
302
|
-
</
|
|
326
|
+
</hlm-input-otp>
|
|
303
327
|
@if (error()) {
|
|
304
328
|
<hlm-error id="otp-error">{{ error() }}</hlm-error>
|
|
305
329
|
}
|
|
@@ -366,7 +390,7 @@ export class PasteExample {
|
|
|
366
390
|
|
|
367
391
|
- Always render a visible `<label>` bound via `inputId` — the hidden native input is what AT focuses; without the label the purpose is unclear.
|
|
368
392
|
- `inputMode="numeric"` (digits) vs `text` (alphanumeric) selects the right mobile keyboard; `inputAutocomplete="one-time-code"` lets browsers/OS autofill SMS codes.
|
|
369
|
-
-
|
|
393
|
+
- Associate failures with text (`<hlm-error>` + `hlm-input-otp`'s `[aria-describedby]`), never by slot color alone. The legacy brain component has no native description input. `HlmError` is not a live region; browser DOM/focus tests do not establish screen-reader announcements.
|
|
370
394
|
- Keep `length` aligned with what the backend expects and validate `minLength(length)` on the form control so incomplete codes cannot submit.
|
|
371
395
|
|
|
372
396
|
## Theming / CSS variables
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Directive, ChangeDetectionStrategy, Component,
|
|
2
|
+
import { Directive, input, booleanAttribute, forwardRef, ChangeDetectionStrategy, Component, inject, computed, numberAttribute, NgModule } from '@angular/core';
|
|
3
3
|
import { classes } from '@egose/shadcn-theme-ng/utils';
|
|
4
|
+
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
5
|
+
import * as i1 from '@spartan-ng/brain/field';
|
|
6
|
+
import { BrnFieldControlDescribedBy, provideBrnLabelable, BrnFieldControl } from '@spartan-ng/brain/field';
|
|
7
|
+
import { BrnInputOtp, BrnInputOtpSlot } from '@spartan-ng/brain/input-otp';
|
|
4
8
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
|
5
9
|
import { lucideMinus } from '@ng-icons/lucide';
|
|
6
|
-
import { BrnInputOtpSlot } from '@spartan-ng/brain/input-otp';
|
|
7
10
|
|
|
8
11
|
class HlmInputOtp {
|
|
9
12
|
constructor() {
|
|
@@ -20,6 +23,83 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
20
23
|
}]
|
|
21
24
|
}], ctorParameters: () => [] });
|
|
22
25
|
|
|
26
|
+
/**
|
|
27
|
+
* BrnInputOtp's CVA and editing behavior with declarative native-input ARIA forwarding.
|
|
28
|
+
* Use with HlmInputOtpSlot; the legacy brn-input-otp skin remains available.
|
|
29
|
+
*/
|
|
30
|
+
class HlmInputOtpControl extends BrnInputOtp {
|
|
31
|
+
/** Native input description IDs, merged with enclosing Spartan field descriptions. */
|
|
32
|
+
ariaDescribedBy = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedBy" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
|
|
33
|
+
required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
34
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpControl, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
35
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: HlmInputOtpControl, isStandalone: true, selector: "hlm-input-otp", inputs: { ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
36
|
+
{ provide: BrnInputOtp, useExisting: forwardRef(() => HlmInputOtpControl) },
|
|
37
|
+
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => HlmInputOtpControl), multi: true },
|
|
38
|
+
provideBrnLabelable(HlmInputOtpControl),
|
|
39
|
+
], usesInheritance: true, hostDirectives: [{ directive: i1.BrnFieldControl }, { directive: HlmInputOtp }], ngImport: i0, template: `
|
|
40
|
+
<ng-content />
|
|
41
|
+
<div [style]="containerStyles()">
|
|
42
|
+
<input
|
|
43
|
+
#otpInput
|
|
44
|
+
data-slot="input-otp"
|
|
45
|
+
[id]="inputId()"
|
|
46
|
+
[class]="inputClass()"
|
|
47
|
+
[autocomplete]="inputAutocomplete()"
|
|
48
|
+
[style]="inputStyles()"
|
|
49
|
+
[disabled]="_disabled()"
|
|
50
|
+
[inputMode]="inputMode()"
|
|
51
|
+
[value]="value() ?? ''"
|
|
52
|
+
brnFieldControlDescribedBy
|
|
53
|
+
[aria-describedby]="ariaDescribedBy()"
|
|
54
|
+
[attr.aria-required]="required() || null"
|
|
55
|
+
[attr.aria-invalid]="invalid?.() ? 'true' : null"
|
|
56
|
+
(input)="onInputChange($event)"
|
|
57
|
+
(paste)="onPaste($event)"
|
|
58
|
+
(focus)="_focused.set(true)"
|
|
59
|
+
(blur)="onBlur()"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: BrnFieldControlDescribedBy, selector: "[brnFieldControlDescribedBy]", inputs: ["aria-describedby"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
63
|
+
}
|
|
64
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpControl, decorators: [{
|
|
65
|
+
type: Component,
|
|
66
|
+
args: [{
|
|
67
|
+
selector: 'hlm-input-otp',
|
|
68
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
69
|
+
imports: [BrnFieldControlDescribedBy],
|
|
70
|
+
hostDirectives: [BrnFieldControl, HlmInputOtp],
|
|
71
|
+
providers: [
|
|
72
|
+
{ provide: BrnInputOtp, useExisting: forwardRef(() => HlmInputOtpControl) },
|
|
73
|
+
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => HlmInputOtpControl), multi: true },
|
|
74
|
+
provideBrnLabelable(HlmInputOtpControl),
|
|
75
|
+
],
|
|
76
|
+
template: `
|
|
77
|
+
<ng-content />
|
|
78
|
+
<div [style]="containerStyles()">
|
|
79
|
+
<input
|
|
80
|
+
#otpInput
|
|
81
|
+
data-slot="input-otp"
|
|
82
|
+
[id]="inputId()"
|
|
83
|
+
[class]="inputClass()"
|
|
84
|
+
[autocomplete]="inputAutocomplete()"
|
|
85
|
+
[style]="inputStyles()"
|
|
86
|
+
[disabled]="_disabled()"
|
|
87
|
+
[inputMode]="inputMode()"
|
|
88
|
+
[value]="value() ?? ''"
|
|
89
|
+
brnFieldControlDescribedBy
|
|
90
|
+
[aria-describedby]="ariaDescribedBy()"
|
|
91
|
+
[attr.aria-required]="required() || null"
|
|
92
|
+
[attr.aria-invalid]="invalid?.() ? 'true' : null"
|
|
93
|
+
(input)="onInputChange($event)"
|
|
94
|
+
(paste)="onPaste($event)"
|
|
95
|
+
(focus)="_focused.set(true)"
|
|
96
|
+
(blur)="onBlur()"
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
`,
|
|
100
|
+
}]
|
|
101
|
+
}], propDecorators: { ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }] } });
|
|
102
|
+
|
|
23
103
|
class HlmInputOtpFakeCaret {
|
|
24
104
|
constructor() {
|
|
25
105
|
classes(() => "pointer-events-none absolute inset-0 flex items-center justify-center");
|
|
@@ -74,16 +154,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
74
154
|
}], ctorParameters: () => [] });
|
|
75
155
|
|
|
76
156
|
class HlmInputOtpSlot {
|
|
157
|
+
_control = inject(HlmInputOtpControl, { optional: true });
|
|
158
|
+
_slot = computed(() => this._control?.context()[this.index()], /* @ts-ignore */
|
|
159
|
+
...(ngDevMode ? [{ debugName: "_slot" }] : /* istanbul ignore next */ []));
|
|
77
160
|
/** The index of the slot to render the char or a fake caret */
|
|
78
161
|
index = input.required({ ...(ngDevMode ? { debugName: "index" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
79
162
|
constructor() {
|
|
80
|
-
classes(() => "dark:bg-input/30 border-input has-[
|
|
163
|
+
classes(() => "dark:bg-input/30 border-input has-[[data-active=\"true\"]]:border-ring has-[[data-active=\"true\"]]:ring-ring/50 has-[[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:ring-destructive/20 dark:has-[[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:ring-destructive/40 data-[matches-spartan-invalid=true]:border-destructive has-[[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:border-destructive size-9 border-y border-e text-sm shadow-xs transition-all outline-none first:rounded-s-md first:border-s last:rounded-e-md has-[[data-active=\"true\"]]:ring-3 relative flex items-center justify-center has-[[data-active=\"true\"]]:z-10");
|
|
81
164
|
}
|
|
82
165
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpSlot, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
83
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
|
|
166
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmInputOtpSlot, isStandalone: true, selector: "hlm-input-otp-slot", inputs: { index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "data-slot": "input-otp-slot" } }, ngImport: i0, template: `
|
|
167
|
+
@if (_control) {
|
|
168
|
+
<span
|
|
169
|
+
[attr.data-active]="_slot()?.isActive"
|
|
170
|
+
[attr.data-invalid]="_control.invalid?.() ? 'true' : null"
|
|
171
|
+
[attr.data-touched]="_control.touched?.() ? 'true' : null"
|
|
172
|
+
[attr.data-dirty]="_control.dirty?.() ? 'true' : null"
|
|
173
|
+
[attr.data-matches-spartan-invalid]="_control.spartanInvalid?.() ? 'true' : null"
|
|
174
|
+
>
|
|
175
|
+
{{ _slot()?.char }}
|
|
176
|
+
@if (_slot()?.hasFakeCaret) {
|
|
177
|
+
<hlm-input-otp-fake-caret />
|
|
178
|
+
}
|
|
179
|
+
</span>
|
|
180
|
+
} @else {
|
|
181
|
+
<brn-input-otp-slot [index]="index()">
|
|
182
|
+
<hlm-input-otp-fake-caret />
|
|
183
|
+
</brn-input-otp-slot>
|
|
184
|
+
}
|
|
87
185
|
`, isInline: true, dependencies: [{ kind: "component", type: BrnInputOtpSlot, selector: "brn-input-otp-slot", inputs: ["index", "forceInvalid"] }, { kind: "component", type: HlmInputOtpFakeCaret, selector: "hlm-input-otp-fake-caret" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
88
186
|
}
|
|
89
187
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpSlot, decorators: [{
|
|
@@ -94,9 +192,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
94
192
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
95
193
|
host: { 'data-slot': 'input-otp-slot' },
|
|
96
194
|
template: `
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
195
|
+
@if (_control) {
|
|
196
|
+
<span
|
|
197
|
+
[attr.data-active]="_slot()?.isActive"
|
|
198
|
+
[attr.data-invalid]="_control.invalid?.() ? 'true' : null"
|
|
199
|
+
[attr.data-touched]="_control.touched?.() ? 'true' : null"
|
|
200
|
+
[attr.data-dirty]="_control.dirty?.() ? 'true' : null"
|
|
201
|
+
[attr.data-matches-spartan-invalid]="_control.spartanInvalid?.() ? 'true' : null"
|
|
202
|
+
>
|
|
203
|
+
{{ _slot()?.char }}
|
|
204
|
+
@if (_slot()?.hasFakeCaret) {
|
|
205
|
+
<hlm-input-otp-fake-caret />
|
|
206
|
+
}
|
|
207
|
+
</span>
|
|
208
|
+
} @else {
|
|
209
|
+
<brn-input-otp-slot [index]="index()">
|
|
210
|
+
<hlm-input-otp-fake-caret />
|
|
211
|
+
</brn-input-otp-slot>
|
|
212
|
+
}
|
|
100
213
|
`,
|
|
101
214
|
}]
|
|
102
215
|
}], ctorParameters: () => [], propDecorators: { index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: true }] }] } });
|
|
@@ -106,6 +219,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
106
219
|
*/
|
|
107
220
|
const HlmInputOtpImports = [
|
|
108
221
|
HlmInputOtp,
|
|
222
|
+
HlmInputOtpControl,
|
|
109
223
|
HlmInputOtpGroup,
|
|
110
224
|
HlmInputOtpSeparator,
|
|
111
225
|
HlmInputOtpSlot,
|
|
@@ -114,10 +228,12 @@ const HlmInputOtpImports = [
|
|
|
114
228
|
class HlmInputOtpModule {
|
|
115
229
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
116
230
|
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpModule, imports: [HlmInputOtp,
|
|
231
|
+
HlmInputOtpControl,
|
|
117
232
|
HlmInputOtpGroup,
|
|
118
233
|
HlmInputOtpSeparator,
|
|
119
234
|
HlmInputOtpSlot,
|
|
120
235
|
HlmInputOtpFakeCaret], exports: [HlmInputOtp,
|
|
236
|
+
HlmInputOtpControl,
|
|
121
237
|
HlmInputOtpGroup,
|
|
122
238
|
HlmInputOtpSeparator,
|
|
123
239
|
HlmInputOtpSlot,
|
|
@@ -137,5 +253,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
137
253
|
* Generated bundle index. Do not edit.
|
|
138
254
|
*/
|
|
139
255
|
|
|
140
|
-
export { HlmInputOtp, HlmInputOtpFakeCaret, HlmInputOtpGroup, HlmInputOtpImports, HlmInputOtpModule, HlmInputOtpSeparator, HlmInputOtpSlot };
|
|
256
|
+
export { HlmInputOtp, HlmInputOtpControl, HlmInputOtpFakeCaret, HlmInputOtpGroup, HlmInputOtpImports, HlmInputOtpModule, HlmInputOtpSeparator, HlmInputOtpSlot };
|
|
141
257
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { NumberInput } from '@angular/cdk/coercion';
|
|
2
|
+
import { BooleanInput, NumberInput } from '@angular/cdk/coercion';
|
|
3
|
+
import { BrnInputOtp } from '@spartan-ng/brain/input-otp';
|
|
4
|
+
import * as i1 from '@spartan-ng/brain/field';
|
|
3
5
|
|
|
4
6
|
declare class HlmInputOtp {
|
|
5
7
|
constructor();
|
|
@@ -7,6 +9,18 @@ declare class HlmInputOtp {
|
|
|
7
9
|
static ɵdir: i0.ɵɵDirectiveDeclaration<HlmInputOtp, "brn-input-otp[hlmInputOtp], brn-input-otp[hlm]", never, {}, {}, never, never, true, never>;
|
|
8
10
|
}
|
|
9
11
|
|
|
12
|
+
/**
|
|
13
|
+
* BrnInputOtp's CVA and editing behavior with declarative native-input ARIA forwarding.
|
|
14
|
+
* Use with HlmInputOtpSlot; the legacy brn-input-otp skin remains available.
|
|
15
|
+
*/
|
|
16
|
+
declare class HlmInputOtpControl extends BrnInputOtp {
|
|
17
|
+
/** Native input description IDs, merged with enclosing Spartan field descriptions. */
|
|
18
|
+
readonly ariaDescribedBy: i0.InputSignal<string | null>;
|
|
19
|
+
readonly required: i0.InputSignalWithTransform<boolean, BooleanInput>;
|
|
20
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<HlmInputOtpControl, never>;
|
|
21
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<HlmInputOtpControl, "hlm-input-otp", never, { "ariaDescribedBy": { "alias": "aria-describedby"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, [{ directive: typeof i1.BrnFieldControl; inputs: {}; outputs: {}; }, { directive: typeof HlmInputOtp; inputs: {}; outputs: {}; }]>;
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
declare class HlmInputOtpFakeCaret {
|
|
11
25
|
constructor();
|
|
12
26
|
static ɵfac: i0.ɵɵFactoryDeclaration<HlmInputOtpFakeCaret, never>;
|
|
@@ -26,6 +40,12 @@ declare class HlmInputOtpSeparator {
|
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
declare class HlmInputOtpSlot {
|
|
43
|
+
protected readonly _control: HlmInputOtpControl | null;
|
|
44
|
+
protected readonly _slot: i0.Signal<{
|
|
45
|
+
char: string | null;
|
|
46
|
+
isActive: boolean;
|
|
47
|
+
hasFakeCaret: boolean;
|
|
48
|
+
} | undefined>;
|
|
29
49
|
/** The index of the slot to render the char or a fake caret */
|
|
30
50
|
readonly index: i0.InputSignalWithTransform<number, NumberInput>;
|
|
31
51
|
constructor();
|
|
@@ -33,11 +53,11 @@ declare class HlmInputOtpSlot {
|
|
|
33
53
|
static ɵcmp: i0.ɵɵComponentDeclaration<HlmInputOtpSlot, "hlm-input-otp-slot", never, { "index": { "alias": "index"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
34
54
|
}
|
|
35
55
|
|
|
36
|
-
declare const HlmInputOtpImports: readonly [typeof HlmInputOtp, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret];
|
|
56
|
+
declare const HlmInputOtpImports: readonly [typeof HlmInputOtp, typeof HlmInputOtpControl, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret];
|
|
37
57
|
declare class HlmInputOtpModule {
|
|
38
58
|
static ɵfac: i0.ɵɵFactoryDeclaration<HlmInputOtpModule, never>;
|
|
39
|
-
static ɵmod: i0.ɵɵNgModuleDeclaration<HlmInputOtpModule, never, [typeof HlmInputOtp, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret], [typeof HlmInputOtp, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret]>;
|
|
59
|
+
static ɵmod: i0.ɵɵNgModuleDeclaration<HlmInputOtpModule, never, [typeof HlmInputOtp, typeof HlmInputOtpControl, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret], [typeof HlmInputOtp, typeof HlmInputOtpControl, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret]>;
|
|
40
60
|
static ɵinj: i0.ɵɵInjectorDeclaration<HlmInputOtpModule>;
|
|
41
61
|
}
|
|
42
62
|
|
|
43
|
-
export { HlmInputOtp, HlmInputOtpFakeCaret, HlmInputOtpGroup, HlmInputOtpImports, HlmInputOtpModule, HlmInputOtpSeparator, HlmInputOtpSlot };
|
|
63
|
+
export { HlmInputOtp, HlmInputOtpControl, HlmInputOtpFakeCaret, HlmInputOtpGroup, HlmInputOtpImports, HlmInputOtpModule, HlmInputOtpSeparator, HlmInputOtpSlot };
|
package/package.json
CHANGED
package/pagination/README.md
CHANGED
|
@@ -144,12 +144,21 @@ For the `tw:` build, swap the specifier to `@egose/shadcn-theme-ng-tw/pagination
|
|
|
144
144
|
| `currentPage` | **model (required)** | `number` | Two-way: `[(currentPage)]`. Clicks set it; out-of-range values are auto-corrected via `outOfBoundCorrection` |
|
|
145
145
|
| `itemsPerPage` | **model (required)** | `number` | Two-way: `[(itemsPerPage)]`, bound to the page-size `hlm-select` |
|
|
146
146
|
| `totalItems` | input (required) | `number` (number coercion) | Server or client total; `0`/negative collapses to a single empty page |
|
|
147
|
-
| `maxSize` | input | `number`, default `7` (number coercion) |
|
|
147
|
+
| `maxSize` | input | `number`, default `7` (number coercion) | Window entries including ellipses; floored and bounded to 1–100; invalid/non-positive uses 7 |
|
|
148
148
|
| `showEdges` | input | `boolean`, default `true` (boolean coercion) | Shows prev/next edge buttons (hidden on first/last page respectively) |
|
|
149
149
|
| `pageSizes` | input | `number[]`, default `[10, 20, 50, 100]` | Page-size menu; the current size is merged in + sorted if missing |
|
|
150
150
|
|
|
151
151
|
Layout: `flex justify-between` row — left count summary (`<b>total</b> total items | <b>pages</b> pages`), center pager, right `hlm-select` page-size picker. Previous/next are plain click handlers (`goToPrevious/goToNext`); page links set `currentPage` directly without routing.
|
|
152
152
|
|
|
153
|
+
#### Normalization and numeric limits (both numbered pagers)
|
|
154
|
+
|
|
155
|
+
- Page count is `ceil(totalItems / itemsPerPage)`, at least 1 and at most `Number.MAX_SAFE_INTEGER`. Finite positive fractional totals/sizes retain that division behavior, including sizes below 1. Overflowing positive division saturates at the safe-integer page limit.
|
|
156
|
+
- Empty, negative or non-finite totals use one page. Non-positive or non-finite sizes (`0`, negative, `NaN`, `Infinity`) also use one page, **without rewriting `itemsPerPage`**. Supply a finite positive size to resume paging. The models are numeric; template number attributes such as `totalItems` and `maxSize` are coerced by Angular.
|
|
157
|
+
- Finite `currentPage` values are floored and clamped to `[1, lastPage]`; non-finite values become 1. Changes to totals, size or the external page are reconciled by an effect, emitting `currentPageChange` once per correction. Valid models emit nothing. Bind `[(currentPage)]` to receive corrections.
|
|
158
|
+
- All derived signals, including the page list, are side-effect-free. Rendering uses the corrected page immediately. Previous/next disappear at their bounds, and direct client-state navigation handlers are also clamped. Empty results show active page 1 and neither edge.
|
|
159
|
+
- `maxSize` / helper `paginationRange` is a window-entry budget, including ellipses. Finite positive values are floored and clamped to **1–100**; non-finite/non-positive values fall back to **7**. Ranges below 5 use a contiguous window containing the active page; larger ranges retain the first/last/ellipsis layout. Work and allocation are proportional to `min(pageCount, normalizedRange)`, never to an unbounded total or range. The array-returning helper API is unchanged.
|
|
160
|
+
- The summary displays the supplied total; normalization controls page count/navigation and does not repair the application's data. For asynchronous server loading, keep the last known total until the new count arrives if a temporary zero should not reset the page.
|
|
161
|
+
|
|
153
162
|
### `HlmNumberedPaginationQueryParams` (`hlm-numbered-pagination-query-params`)
|
|
154
163
|
|
|
155
164
|
Same models/inputs as `HlmNumberedPagination` **plus**:
|
|
@@ -160,11 +169,13 @@ Same models/inputs as `HlmNumberedPagination` **plus**:
|
|
|
160
169
|
|
|
161
170
|
Every link (prev/numbered/next) navigates via `RouterLink` with `[queryParams]="{ page }"` and `queryParamsHandling="merge"`. The active page renders with `link === undefined` (no navigation) + `isActive`.
|
|
162
171
|
|
|
172
|
+
The component **does not subscribe to the route or navigate on correction/size changes**. The parent reads `?page=` and supplies `currentPage`; link clicks navigate, then the parent's route subscription updates that model. Corrections emit `currentPageChange` but leave the URL unchanged. To canonicalize a corrected page in the URL, handle that output in the parent with `router.navigate([], { relativeTo: route, queryParams: { page }, queryParamsHandling: 'merge', replaceUrl: true })`. Keep unrelated parameters when doing so. Page-size URL persistence is also parent-owned.
|
|
173
|
+
|
|
163
174
|
### Helpers
|
|
164
175
|
|
|
165
176
|
| Symbol | Signature | Notes |
|
|
166
177
|
| ------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
167
|
-
| `outOfBoundCorrection(totalItems, itemsPerPage, currentPage)` | `(number, number, number) => number` |
|
|
178
|
+
| `outOfBoundCorrection(totalItems, itemsPerPage, currentPage)` | `(number, number, number) => number` | Pure floor/clamp using the normalization policy above; empty/invalid paging returns 1 |
|
|
168
179
|
| `createPageArray(currentPage, itemsPerPage, totalItems, paginationRange)` | `(number, number, number, number) => (number \| '...')[]` | Window with `'...'` gaps; powers both numbered pagers |
|
|
169
180
|
| `Page` | `type Page = number \| '...'` | Not exported from public-api (internal to the pager file) — redeclare locally if you type custom windows |
|
|
170
181
|
|
|
@@ -246,7 +257,8 @@ export class PagerClientComponent {
|
|
|
246
257
|
### 3. Router `?page=` pager (server-driven)
|
|
247
258
|
|
|
248
259
|
```ts
|
|
249
|
-
import { Component, signal
|
|
260
|
+
import { Component, signal } from '@angular/core';
|
|
261
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
250
262
|
import { ActivatedRoute } from '@angular/router';
|
|
251
263
|
import { HlmPaginationImports } from '@egose/shadcn-theme-ng/pagination';
|
|
252
264
|
|
|
@@ -261,7 +273,7 @@ import { HlmPaginationImports } from '@egose/shadcn-theme-ng/pagination';
|
|
|
261
273
|
[totalItems]="total()"
|
|
262
274
|
link="."
|
|
263
275
|
/>
|
|
264
|
-
<p class="tw:text-sm tw:text-muted-foreground">
|
|
276
|
+
<p class="tw:text-sm tw:text-muted-foreground">Displayed page: {{ page() }}.</p>
|
|
265
277
|
`,
|
|
266
278
|
})
|
|
267
279
|
export class PagerQueryComponent {
|
|
@@ -270,13 +282,10 @@ export class PagerQueryComponent {
|
|
|
270
282
|
readonly total = signal(320);
|
|
271
283
|
|
|
272
284
|
constructor(route: ActivatedRoute) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this.page.set(Number.isFinite(p) && p >= 1 ? Math.floor(p) : 1);
|
|
278
|
-
});
|
|
279
|
-
return () => sub.unsubscribe();
|
|
285
|
+
// Read deep links and back/forward changes; the pager corrects model bounds.
|
|
286
|
+
// Corrections here update the displayed page, not the URL.
|
|
287
|
+
route.queryParamMap.pipe(takeUntilDestroyed()).subscribe((params) => {
|
|
288
|
+
this.page.set(Number(params.get('page') ?? 1));
|
|
280
289
|
});
|
|
281
290
|
}
|
|
282
291
|
}
|
|
@@ -330,7 +339,7 @@ import { HlmPaginationImports, createPageArray } from '@egose/shadcn-theme-ng/pa
|
|
|
330
339
|
template: `
|
|
331
340
|
<nav hlmPagination>
|
|
332
341
|
<ul hlmPaginationContent>
|
|
333
|
-
@for (p of window(); track
|
|
342
|
+
@for (p of window(); track $index) {
|
|
334
343
|
<li hlmPaginationItem>
|
|
335
344
|
@if (p === '...') {
|
|
336
345
|
<hlm-pagination-ellipsis />
|