@nanahoshi/mona-ui 0.17.1 → 0.18.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/fesm2022/nanahoshi-mona-ui-internal.mjs +12 -0
- package/fesm2022/nanahoshi-mona-ui-otp-input.mjs +817 -0
- package/fesm2022/nanahoshi-mona-ui-segmented.mjs +39 -1
- package/otp-input/package.json +4 -0
- package/package.json +5 -1
- package/types/nanahoshi-mona-ui-otp-input.d.ts +209 -0
- package/types/nanahoshi-mona-ui-segmented.d.ts +7 -0
|
@@ -7,13 +7,21 @@ import { isIterable } from 'rxjs/internal/util/isIterable';
|
|
|
7
7
|
|
|
8
8
|
class AttributeBinderDirective {
|
|
9
9
|
#element;
|
|
10
|
+
#previousKeys;
|
|
10
11
|
#renderer;
|
|
11
12
|
constructor() {
|
|
12
13
|
this.#element = inject(ElementRef);
|
|
14
|
+
this.#previousKeys = new Set();
|
|
13
15
|
this.#renderer = inject(Renderer2);
|
|
14
16
|
this.attributes = input({}, { ...(ngDevMode ? { debugName: "attributes" } : /* istanbul ignore next */ {}), alias: "monaAttributeBinder" });
|
|
15
17
|
effect(() => {
|
|
16
18
|
const attributes = this.attributes();
|
|
19
|
+
const nextKeys = new Set(Object.keys(attributes));
|
|
20
|
+
for (const prevKey of this.#previousKeys) {
|
|
21
|
+
if (!nextKeys.has(prevKey)) {
|
|
22
|
+
this.#renderer.removeAttribute(this.#element.nativeElement, prevKey);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
17
25
|
for (const [key, value] of Object.entries(attributes)) {
|
|
18
26
|
if (value == null || value === false) {
|
|
19
27
|
this.#renderer.removeAttribute(this.#element.nativeElement, key);
|
|
@@ -23,6 +31,10 @@ class AttributeBinderDirective {
|
|
|
23
31
|
this.#renderer.setAttribute(this.#element.nativeElement, key, attributeValue);
|
|
24
32
|
}
|
|
25
33
|
}
|
|
34
|
+
this.#previousKeys.clear();
|
|
35
|
+
for (const key of nextKeys) {
|
|
36
|
+
this.#previousKeys.add(key);
|
|
37
|
+
}
|
|
26
38
|
});
|
|
27
39
|
}
|
|
28
40
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AttributeBinderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { inject, TemplateRef, Directive, computed, signal, viewChild, contentChild, input, output, model, effect, Component } from '@angular/core';
|
|
4
|
+
import { themeControlSurfaceClasses, AttributeBinderDirective } from '@nanahoshi/mona-ui/internal';
|
|
5
|
+
import { twMerge } from 'tailwind-merge';
|
|
6
|
+
import { cva } from 'class-variance-authority';
|
|
7
|
+
|
|
8
|
+
class OtpInputSeparatorTemplateDirective {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.templateRef = inject((TemplateRef));
|
|
11
|
+
}
|
|
12
|
+
static ngTemplateContextGuard(_directive, _context) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: OtpInputSeparatorTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
16
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: OtpInputSeparatorTemplateDirective, isStandalone: true, selector: "ng-template[monaOtpInputSeparatorTemplate]", ngImport: i0 }); }
|
|
17
|
+
}
|
|
18
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: OtpInputSeparatorTemplateDirective, decorators: [{
|
|
19
|
+
type: Directive,
|
|
20
|
+
args: [{
|
|
21
|
+
selector: "ng-template[monaOtpInputSeparatorTemplate]"
|
|
22
|
+
}]
|
|
23
|
+
}] });
|
|
24
|
+
|
|
25
|
+
const otpInputHostThemeVariants = cva(`
|
|
26
|
+
relative inline-flex items-center gap-2
|
|
27
|
+
outline-none
|
|
28
|
+
transition-[color,box-shadow,border] duration-(--mona-motion-standard) ease-in-out
|
|
29
|
+
data-[disabled='true']:pointer-events-none
|
|
30
|
+
data-[disabled='true']:cursor-not-allowed
|
|
31
|
+
data-[readonly='true']:cursor-default
|
|
32
|
+
`);
|
|
33
|
+
const otpInputFieldThemeVariants = cva(`pointer-events-none absolute inset-0 size-full opacity-0 outline-none select-none -z-10`);
|
|
34
|
+
const otpInputSlotThemeVariants = cva(`
|
|
35
|
+
relative inline-flex items-center justify-center
|
|
36
|
+
text-center font-mono tabular-nums select-none
|
|
37
|
+
overflow-hidden
|
|
38
|
+
${themeControlSurfaceClasses} text-foreground
|
|
39
|
+
border border-input-border shadow-(--shadow-control)
|
|
40
|
+
outline-none
|
|
41
|
+
transition-[color,box-shadow,border,background-color] duration-(--mona-motion-standard) ease-in-out
|
|
42
|
+
|
|
43
|
+
data-[disabled='true']:pointer-events-none
|
|
44
|
+
data-[disabled='true']:cursor-not-allowed
|
|
45
|
+
data-[disabled='true']:border-disabled-border
|
|
46
|
+
data-[disabled='true']:bg-disabled-background
|
|
47
|
+
data-[disabled='true']:text-disabled-foreground
|
|
48
|
+
data-[disabled='true']:shadow-none
|
|
49
|
+
|
|
50
|
+
data-[readonly='true']:cursor-default
|
|
51
|
+
|
|
52
|
+
data-[active='true']:border-focus-indicator
|
|
53
|
+
data-[active='true']:ring-2 data-[active='true']:ring-focus-indicator/35
|
|
54
|
+
data-[active='true']:z-10
|
|
55
|
+
|
|
56
|
+
data-[selected='true']:bg-primary/10
|
|
57
|
+
|
|
58
|
+
data-[placeholder='true']:text-muted-foreground
|
|
59
|
+
|
|
60
|
+
data-[invalid='true']:border-error
|
|
61
|
+
data-[invalid='true']:data-[active='true']:border-error
|
|
62
|
+
data-[invalid='true']:data-[active='true']:ring-2
|
|
63
|
+
data-[invalid='true']:data-[active='true']:ring-error/35
|
|
64
|
+
`, {
|
|
65
|
+
variants: {
|
|
66
|
+
rounded: {
|
|
67
|
+
full: "rounded-full",
|
|
68
|
+
large: "rounded-lg",
|
|
69
|
+
medium: "rounded-md",
|
|
70
|
+
none: "rounded-none",
|
|
71
|
+
small: "rounded-sm"
|
|
72
|
+
},
|
|
73
|
+
size: {
|
|
74
|
+
large: "h-10 w-10 min-w-10 text-md",
|
|
75
|
+
medium: "h-9 w-9 min-w-9 text-sm",
|
|
76
|
+
small: "h-8 w-8 min-w-8 text-xs"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
defaultVariants: {
|
|
80
|
+
rounded: "medium",
|
|
81
|
+
size: "medium"
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const RESERVED_INPUT_ATTRIBUTES = new Set([
|
|
86
|
+
"aria-hidden",
|
|
87
|
+
"aria-invalid",
|
|
88
|
+
"aria-required",
|
|
89
|
+
"class",
|
|
90
|
+
"disabled",
|
|
91
|
+
"maxlength",
|
|
92
|
+
"readonly",
|
|
93
|
+
"required",
|
|
94
|
+
"role",
|
|
95
|
+
"style",
|
|
96
|
+
"type",
|
|
97
|
+
"value"
|
|
98
|
+
]);
|
|
99
|
+
const COMPONENT_MANAGED_ATTRIBUTES = new Set([
|
|
100
|
+
"aria-label",
|
|
101
|
+
"aria-labelledby",
|
|
102
|
+
"autocomplete",
|
|
103
|
+
"inputmode"
|
|
104
|
+
]);
|
|
105
|
+
function findAttribute(attrs, name) {
|
|
106
|
+
const target = name.toLowerCase();
|
|
107
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
108
|
+
if (key.toLowerCase() === target) {
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
function findUsableAttribute(attrs, name) {
|
|
115
|
+
const value = findAttribute(attrs, name);
|
|
116
|
+
if (value == null || value === false) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
function toNonEmptyString(value) {
|
|
122
|
+
if (value == null || value === false) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const text = String(value).trim();
|
|
126
|
+
return text.length > 0 ? text : null;
|
|
127
|
+
}
|
|
128
|
+
function normalizeLength(length) {
|
|
129
|
+
if (typeof length !== "number" || isNaN(length) || !isFinite(length) || length < 1) {
|
|
130
|
+
return 4;
|
|
131
|
+
}
|
|
132
|
+
return Math.floor(length);
|
|
133
|
+
}
|
|
134
|
+
function isValidCharacter(char, type, characterPattern) {
|
|
135
|
+
if (!char || char.length !== 1) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (characterPattern != null) {
|
|
139
|
+
const sanitized = new RegExp(characterPattern.source, characterPattern.flags.replace(/[gy]/g, ""));
|
|
140
|
+
return sanitized.test(char);
|
|
141
|
+
}
|
|
142
|
+
if (type === "number") {
|
|
143
|
+
return /^[0-9]$/.test(char);
|
|
144
|
+
}
|
|
145
|
+
return /^[a-zA-Z0-9]$/.test(char);
|
|
146
|
+
}
|
|
147
|
+
function filterCharacters(raw, type, characterPattern, maxLength) {
|
|
148
|
+
if (!raw) {
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
let result = "";
|
|
152
|
+
for (const char of raw) {
|
|
153
|
+
if (isValidCharacter(char, type, characterPattern)) {
|
|
154
|
+
result += char;
|
|
155
|
+
if (result.length >= maxLength) {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
function normalizeGroupLengths(groupLength, totalLength) {
|
|
163
|
+
if (totalLength <= 0) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
if (groupLength == null) {
|
|
167
|
+
return [totalLength];
|
|
168
|
+
}
|
|
169
|
+
if (typeof groupLength === "number") {
|
|
170
|
+
if (!isFinite(groupLength) || isNaN(groupLength) || groupLength <= 0) {
|
|
171
|
+
return [totalLength];
|
|
172
|
+
}
|
|
173
|
+
const size = Math.floor(groupLength);
|
|
174
|
+
if (size <= 0) {
|
|
175
|
+
return [totalLength];
|
|
176
|
+
}
|
|
177
|
+
const groups = [];
|
|
178
|
+
let remaining = totalLength;
|
|
179
|
+
while (remaining > 0) {
|
|
180
|
+
const g = Math.min(size, remaining);
|
|
181
|
+
groups.push(g);
|
|
182
|
+
remaining -= g;
|
|
183
|
+
}
|
|
184
|
+
return groups;
|
|
185
|
+
}
|
|
186
|
+
if (Array.isArray(groupLength)) {
|
|
187
|
+
const groups = [];
|
|
188
|
+
let remaining = totalLength;
|
|
189
|
+
for (const item of groupLength) {
|
|
190
|
+
if (typeof item === "number" && isFinite(item) && !isNaN(item) && item > 0) {
|
|
191
|
+
const size = Math.floor(item);
|
|
192
|
+
if (size > 0) {
|
|
193
|
+
const g = Math.min(size, remaining);
|
|
194
|
+
groups.push(g);
|
|
195
|
+
remaining -= g;
|
|
196
|
+
if (remaining <= 0) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (remaining > 0) {
|
|
203
|
+
groups.push(remaining);
|
|
204
|
+
}
|
|
205
|
+
return groups.length > 0 ? groups : [totalLength];
|
|
206
|
+
}
|
|
207
|
+
return [totalLength];
|
|
208
|
+
}
|
|
209
|
+
function sanitizeInputAttributes(attrs) {
|
|
210
|
+
const sanitized = {};
|
|
211
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
212
|
+
const normalizedKey = key.toLowerCase();
|
|
213
|
+
if (!RESERVED_INPUT_ATTRIBUTES.has(normalizedKey) && !COMPONENT_MANAGED_ATTRIBUTES.has(normalizedKey)) {
|
|
214
|
+
sanitized[key] = val;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return sanitized;
|
|
218
|
+
}
|
|
219
|
+
function getSlotRoundedClasses(rounded, firstSlot, lastSlot, groupSize) {
|
|
220
|
+
if (groupSize <= 1) {
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
if (firstSlot) {
|
|
224
|
+
switch (rounded) {
|
|
225
|
+
case "none":
|
|
226
|
+
return "rounded-none";
|
|
227
|
+
case "small":
|
|
228
|
+
return "rounded-s-sm rounded-e-none";
|
|
229
|
+
case "medium":
|
|
230
|
+
return "rounded-s-md rounded-e-none";
|
|
231
|
+
case "large":
|
|
232
|
+
return "rounded-s-lg rounded-e-none";
|
|
233
|
+
case "full":
|
|
234
|
+
return "rounded-s-full rounded-e-none";
|
|
235
|
+
default:
|
|
236
|
+
return "rounded-s-md rounded-e-none";
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (lastSlot) {
|
|
240
|
+
switch (rounded) {
|
|
241
|
+
case "none":
|
|
242
|
+
return "rounded-none";
|
|
243
|
+
case "small":
|
|
244
|
+
return "rounded-e-sm rounded-s-none";
|
|
245
|
+
case "medium":
|
|
246
|
+
return "rounded-e-md rounded-s-none";
|
|
247
|
+
case "large":
|
|
248
|
+
return "rounded-e-lg rounded-s-none";
|
|
249
|
+
case "full":
|
|
250
|
+
return "rounded-e-full rounded-s-none";
|
|
251
|
+
default:
|
|
252
|
+
return "rounded-e-md rounded-s-none";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return "rounded-none";
|
|
256
|
+
}
|
|
257
|
+
function computeSlotClasses(options) {
|
|
258
|
+
const { firstSlot, groupSize, lastSlot, rounded, size, slotClass, spacing } = options;
|
|
259
|
+
const baseClasses = otpInputSlotThemeVariants({
|
|
260
|
+
rounded: spacing || groupSize === 1 ? rounded : "none",
|
|
261
|
+
size
|
|
262
|
+
});
|
|
263
|
+
let extraRounding = "";
|
|
264
|
+
if (!spacing) {
|
|
265
|
+
extraRounding = getSlotRoundedClasses(rounded, firstSlot, lastSlot, groupSize);
|
|
266
|
+
}
|
|
267
|
+
return twMerge(baseClasses, extraRounding, slotClass);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
class OtpInputComponent {
|
|
271
|
+
constructor() {
|
|
272
|
+
this.activeSlotIndex = computed(() => {
|
|
273
|
+
if (!this.hasFocus()) {
|
|
274
|
+
return -1;
|
|
275
|
+
}
|
|
276
|
+
const totalLen = this.normalizedLength();
|
|
277
|
+
const start = this.logicalSelectionStart();
|
|
278
|
+
const end = this.logicalSelectionEnd();
|
|
279
|
+
if (start === end) {
|
|
280
|
+
if (start < totalLen) {
|
|
281
|
+
return start;
|
|
282
|
+
}
|
|
283
|
+
return Math.max(0, totalLen - 1);
|
|
284
|
+
}
|
|
285
|
+
return Math.min(start, totalLen - 1);
|
|
286
|
+
}, /* @ts-ignore */
|
|
287
|
+
...(ngDevMode ? [{ debugName: "activeSlotIndex" }] : /* istanbul ignore next */ []));
|
|
288
|
+
this.classes = computed(() => {
|
|
289
|
+
const baseClasses = otpInputHostThemeVariants();
|
|
290
|
+
return twMerge(baseClasses, this.userClass());
|
|
291
|
+
}, /* @ts-ignore */
|
|
292
|
+
...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
|
|
293
|
+
this.computedAriaLabel = computed(() => {
|
|
294
|
+
const labelledby = this.computedAriaLabelledby();
|
|
295
|
+
if (labelledby != null) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const attrs = this.inputAttributes();
|
|
299
|
+
const rawAttrLabel = findUsableAttribute(attrs, "aria-label");
|
|
300
|
+
const customAttrLabel = toNonEmptyString(rawAttrLabel);
|
|
301
|
+
if (customAttrLabel != null) {
|
|
302
|
+
return customAttrLabel;
|
|
303
|
+
}
|
|
304
|
+
const fallback = toNonEmptyString(this.ariaLabel());
|
|
305
|
+
return fallback ?? "Verification code";
|
|
306
|
+
}, /* @ts-ignore */
|
|
307
|
+
...(ngDevMode ? [{ debugName: "computedAriaLabel" }] : /* istanbul ignore next */ []));
|
|
308
|
+
this.computedAriaLabelledby = computed(() => {
|
|
309
|
+
const attrs = this.inputAttributes();
|
|
310
|
+
const raw = findUsableAttribute(attrs, "aria-labelledby");
|
|
311
|
+
return toNonEmptyString(raw);
|
|
312
|
+
}, /* @ts-ignore */
|
|
313
|
+
...(ngDevMode ? [{ debugName: "computedAriaLabelledby" }] : /* istanbul ignore next */ []));
|
|
314
|
+
this.derivedAutocomplete = computed(() => {
|
|
315
|
+
const attrs = this.inputAttributes();
|
|
316
|
+
const autocomplete = findUsableAttribute(attrs, "autocomplete");
|
|
317
|
+
return autocomplete != null ? String(autocomplete) : "one-time-code";
|
|
318
|
+
}, /* @ts-ignore */
|
|
319
|
+
...(ngDevMode ? [{ debugName: "derivedAutocomplete" }] : /* istanbul ignore next */ []));
|
|
320
|
+
this.derivedInputMode = computed(() => {
|
|
321
|
+
const attrs = this.inputAttributes();
|
|
322
|
+
const inputMode = findUsableAttribute(attrs, "inputmode");
|
|
323
|
+
if (inputMode != null) {
|
|
324
|
+
return String(inputMode);
|
|
325
|
+
}
|
|
326
|
+
return this.type() === "number" ? "numeric" : "text";
|
|
327
|
+
}, /* @ts-ignore */
|
|
328
|
+
...(ngDevMode ? [{ debugName: "derivedInputMode" }] : /* istanbul ignore next */ []));
|
|
329
|
+
this.hasFocus = signal(false, /* @ts-ignore */
|
|
330
|
+
...(ngDevMode ? [{ debugName: "hasFocus" }] : /* istanbul ignore next */ []));
|
|
331
|
+
this.inputClass = computed(() => otpInputFieldThemeVariants(), /* @ts-ignore */
|
|
332
|
+
...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
|
|
333
|
+
this.inputRef = viewChild.required("input", /* @ts-ignore */
|
|
334
|
+
...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
|
|
335
|
+
this.invalidState = computed(() => this.touched() && (this.invalid() || (this.required() && this.value().length !== this.normalizedLength())), /* @ts-ignore */
|
|
336
|
+
...(ngDevMode ? [{ debugName: "invalidState" }] : /* istanbul ignore next */ []));
|
|
337
|
+
this.isComplete = computed(() => this.value().length === this.normalizedLength(), /* @ts-ignore */
|
|
338
|
+
...(ngDevMode ? [{ debugName: "isComplete" }] : /* istanbul ignore next */ []));
|
|
339
|
+
this.isComposing = signal(false, /* @ts-ignore */
|
|
340
|
+
...(ngDevMode ? [{ debugName: "isComposing" }] : /* istanbul ignore next */ []));
|
|
341
|
+
this.logicalSelectionEnd = computed(() => {
|
|
342
|
+
const maxLogicalPosition = Math.min(this.value().length, this.normalizedLength());
|
|
343
|
+
const start = this.logicalSelectionStart();
|
|
344
|
+
const rawEnd = Math.max(0, Math.min(this.selectionEnd(), maxLogicalPosition));
|
|
345
|
+
return Math.max(start, rawEnd);
|
|
346
|
+
}, /* @ts-ignore */
|
|
347
|
+
...(ngDevMode ? [{ debugName: "logicalSelectionEnd" }] : /* istanbul ignore next */ []));
|
|
348
|
+
this.logicalSelectionStart = computed(() => {
|
|
349
|
+
const maxLogicalPosition = Math.min(this.value().length, this.normalizedLength());
|
|
350
|
+
return Math.max(0, Math.min(this.selectionStart(), maxLogicalPosition));
|
|
351
|
+
}, /* @ts-ignore */
|
|
352
|
+
...(ngDevMode ? [{ debugName: "logicalSelectionStart" }] : /* istanbul ignore next */ []));
|
|
353
|
+
this.nativeInputType = computed(() => (this.type() === "password" ? "password" : "text"), /* @ts-ignore */
|
|
354
|
+
...(ngDevMode ? [{ debugName: "nativeInputType" }] : /* istanbul ignore next */ []));
|
|
355
|
+
this.normalizedLength = computed(() => normalizeLength(this.length()), /* @ts-ignore */
|
|
356
|
+
...(ngDevMode ? [{ debugName: "normalizedLength" }] : /* istanbul ignore next */ []));
|
|
357
|
+
this.sanitizedAttributes = computed(() => sanitizeInputAttributes(this.inputAttributes()), /* @ts-ignore */
|
|
358
|
+
...(ngDevMode ? [{ debugName: "sanitizedAttributes" }] : /* istanbul ignore next */ []));
|
|
359
|
+
this.selectionEnd = signal(0, /* @ts-ignore */
|
|
360
|
+
...(ngDevMode ? [{ debugName: "selectionEnd" }] : /* istanbul ignore next */ []));
|
|
361
|
+
this.selectionStart = signal(0, /* @ts-ignore */
|
|
362
|
+
...(ngDevMode ? [{ debugName: "selectionStart" }] : /* istanbul ignore next */ []));
|
|
363
|
+
this.separatorTemplate = contentChild(OtpInputSeparatorTemplateDirective, { ...(ngDevMode ? { debugName: "separatorTemplate" } : /* istanbul ignore next */ {}), read: TemplateRef });
|
|
364
|
+
this.visualGroups = computed(() => {
|
|
365
|
+
const totalLen = this.normalizedLength();
|
|
366
|
+
const groupSizes = normalizeGroupLengths(this.groupLength(), totalLen);
|
|
367
|
+
const val = this.value();
|
|
368
|
+
const activeIndex = this.activeSlotIndex();
|
|
369
|
+
const selStart = this.logicalSelectionStart();
|
|
370
|
+
const selEnd = this.logicalSelectionEnd();
|
|
371
|
+
const focused = this.hasFocus();
|
|
372
|
+
const inputType = this.type();
|
|
373
|
+
const placeholderStr = this.placeholder();
|
|
374
|
+
const placeholderChar = placeholderStr.length > 0 ? placeholderStr.charAt(0) : "";
|
|
375
|
+
const rounded = this.rounded();
|
|
376
|
+
const size = this.size();
|
|
377
|
+
const slotClass = this.slotClass();
|
|
378
|
+
const spacing = this.spacing();
|
|
379
|
+
const groups = [];
|
|
380
|
+
let slotCounter = 0;
|
|
381
|
+
for (let gIndex = 0; gIndex < groupSizes.length; gIndex++) {
|
|
382
|
+
const groupSize = groupSizes[gIndex];
|
|
383
|
+
const slots = [];
|
|
384
|
+
for (let s = 0; s < groupSize; s++) {
|
|
385
|
+
const slotIndex = slotCounter++;
|
|
386
|
+
const filled = slotIndex < val.length;
|
|
387
|
+
const character = filled ? val[slotIndex] : "";
|
|
388
|
+
const isPlaceholder = !filled && placeholderChar.length > 0;
|
|
389
|
+
let displayCharacter = "";
|
|
390
|
+
if (filled) {
|
|
391
|
+
displayCharacter = inputType === "password" ? "•" : character;
|
|
392
|
+
}
|
|
393
|
+
else if (isPlaceholder) {
|
|
394
|
+
displayCharacter = placeholderChar;
|
|
395
|
+
}
|
|
396
|
+
const active = focused && slotIndex === activeIndex;
|
|
397
|
+
const selected = focused && slotIndex >= selStart && slotIndex < selEnd;
|
|
398
|
+
const isFirstSlot = s === 0;
|
|
399
|
+
const isLastSlot = s === groupSize - 1;
|
|
400
|
+
const classes = computeSlotClasses({
|
|
401
|
+
firstSlot: isFirstSlot,
|
|
402
|
+
groupSize,
|
|
403
|
+
lastSlot: isLastSlot,
|
|
404
|
+
rounded,
|
|
405
|
+
size,
|
|
406
|
+
slotClass,
|
|
407
|
+
spacing
|
|
408
|
+
});
|
|
409
|
+
slots.push({
|
|
410
|
+
active,
|
|
411
|
+
classes,
|
|
412
|
+
displayCharacter,
|
|
413
|
+
filled,
|
|
414
|
+
index: slotIndex,
|
|
415
|
+
placeholder: isPlaceholder,
|
|
416
|
+
selected
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
groups.push({
|
|
420
|
+
index: gIndex,
|
|
421
|
+
slots
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return groups;
|
|
425
|
+
}, /* @ts-ignore */
|
|
426
|
+
...(ngDevMode ? [{ debugName: "visualGroups" }] : /* istanbul ignore next */ []));
|
|
427
|
+
/**
|
|
428
|
+
* @description Accessible name for the inner `<input>` element.
|
|
429
|
+
* @default "Verification code"
|
|
430
|
+
*/
|
|
431
|
+
this.ariaLabel = input("Verification code", /* @ts-ignore */
|
|
432
|
+
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
433
|
+
/**
|
|
434
|
+
* @description Regular expression used to determine whether each individual OTP character is allowed.
|
|
435
|
+
* @default null
|
|
436
|
+
*/
|
|
437
|
+
this.characterPattern = input(null, /* @ts-ignore */
|
|
438
|
+
...(ngDevMode ? [{ debugName: "characterPattern" }] : /* istanbul ignore next */ []));
|
|
439
|
+
/**
|
|
440
|
+
* @description Emitted when a user interaction completes the verification code.
|
|
441
|
+
*/
|
|
442
|
+
this.complete = output();
|
|
443
|
+
/**
|
|
444
|
+
* @description Renders the component with reduced visual emphasis and removes pointer interaction.
|
|
445
|
+
* When bound to a signal form field via `[formField]`, this is written by the `FormField` directive.
|
|
446
|
+
* @default false
|
|
447
|
+
*/
|
|
448
|
+
this.disabled = input(false, /* @ts-ignore */
|
|
449
|
+
...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
450
|
+
/**
|
|
451
|
+
* @description Configures slot grouping lengths. Accepts a uniform number or an array of group sizes.
|
|
452
|
+
* @default null
|
|
453
|
+
*/
|
|
454
|
+
this.groupLength = input(null, /* @ts-ignore */
|
|
455
|
+
...(ngDevMode ? [{ debugName: "groupLength" }] : /* istanbul ignore next */ []));
|
|
456
|
+
/**
|
|
457
|
+
* @description Additional HTML attributes applied directly to the inner `<input>` element.
|
|
458
|
+
* @default {}
|
|
459
|
+
*/
|
|
460
|
+
this.inputAttributes = input({}, /* @ts-ignore */
|
|
461
|
+
...(ngDevMode ? [{ debugName: "inputAttributes" }] : /* istanbul ignore next */ []));
|
|
462
|
+
/**
|
|
463
|
+
* @description Emitted when the inner input loses focus.
|
|
464
|
+
*/
|
|
465
|
+
this.inputBlur = output();
|
|
466
|
+
/**
|
|
467
|
+
* @description Emitted when the inner input gains focus.
|
|
468
|
+
*/
|
|
469
|
+
this.inputFocus = output();
|
|
470
|
+
/**
|
|
471
|
+
* @description Marks the OTP input as invalid. When bound to a signal form field via `[formField]`,
|
|
472
|
+
* this is written by the `FormField` directive.
|
|
473
|
+
* @default false
|
|
474
|
+
*/
|
|
475
|
+
this.invalid = input(false, /* @ts-ignore */
|
|
476
|
+
...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
477
|
+
/**
|
|
478
|
+
* @description Sets the total number of characters accepted by the OTP input and the number of visual slots.
|
|
479
|
+
* @default 4
|
|
480
|
+
*/
|
|
481
|
+
this.length = input(4, /* @ts-ignore */
|
|
482
|
+
...(ngDevMode ? [{ debugName: "length" }] : /* istanbul ignore next */ []));
|
|
483
|
+
/**
|
|
484
|
+
* @description Hint character displayed in empty visual slots.
|
|
485
|
+
* @default ""
|
|
486
|
+
*/
|
|
487
|
+
this.placeholder = input("", /* @ts-ignore */
|
|
488
|
+
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
489
|
+
/**
|
|
490
|
+
* @description Prevents value changes while preserving the component's focusability and visual state.
|
|
491
|
+
* When bound to a signal form field via `[formField]`, this is written by the `FormField` directive.
|
|
492
|
+
* @default false
|
|
493
|
+
*/
|
|
494
|
+
this.readonly = input(false, /* @ts-ignore */
|
|
495
|
+
...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
|
|
496
|
+
/**
|
|
497
|
+
* @description Marks the OTP input as required. Triggers invalid state when touched and incomplete.
|
|
498
|
+
* When bound to a signal form field via `[formField]`, this is written by the `FormField` directive.
|
|
499
|
+
* @default false
|
|
500
|
+
*/
|
|
501
|
+
this.required = input(false, /* @ts-ignore */
|
|
502
|
+
...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
503
|
+
/**
|
|
504
|
+
* @description Border-radius preset applied to the OTP slots.
|
|
505
|
+
* @default "medium"
|
|
506
|
+
*/
|
|
507
|
+
this.rounded = input("medium", /* @ts-ignore */
|
|
508
|
+
...(ngDevMode ? [{ debugName: "rounded" }] : /* istanbul ignore next */ []));
|
|
509
|
+
/**
|
|
510
|
+
* @description Text character or string displayed between visual slot groups.
|
|
511
|
+
* @default ""
|
|
512
|
+
*/
|
|
513
|
+
this.separator = input("", /* @ts-ignore */
|
|
514
|
+
...(ngDevMode ? [{ debugName: "separator" }] : /* istanbul ignore next */ []));
|
|
515
|
+
/**
|
|
516
|
+
* @description Additional CSS classes applied to separator elements.
|
|
517
|
+
* @default ""
|
|
518
|
+
*/
|
|
519
|
+
this.separatorClass = input("", /* @ts-ignore */
|
|
520
|
+
...(ngDevMode ? [{ debugName: "separatorClass" }] : /* istanbul ignore next */ []));
|
|
521
|
+
/**
|
|
522
|
+
* @description Size preset controlling the dimensions and font size of the OTP slots.
|
|
523
|
+
* @default "medium"
|
|
524
|
+
*/
|
|
525
|
+
this.size = input("medium", /* @ts-ignore */
|
|
526
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
527
|
+
/**
|
|
528
|
+
* @description Additional CSS classes applied to each visual slot element.
|
|
529
|
+
* @default ""
|
|
530
|
+
*/
|
|
531
|
+
this.slotClass = input("", /* @ts-ignore */
|
|
532
|
+
...(ngDevMode ? [{ debugName: "slotClass" }] : /* istanbul ignore next */ []));
|
|
533
|
+
/**
|
|
534
|
+
* @description Determines whether visual slots within a group have spacing between them or are joined together.
|
|
535
|
+
* @default true
|
|
536
|
+
*/
|
|
537
|
+
this.spacing = input(true, /* @ts-ignore */
|
|
538
|
+
...(ngDevMode ? [{ debugName: "spacing" }] : /* istanbul ignore next */ []));
|
|
539
|
+
/**
|
|
540
|
+
* @description Emitted when the OTP input is interacted with — on blur or user value change.
|
|
541
|
+
* The `FormField` directive listens to this to mark the field as touched.
|
|
542
|
+
*/
|
|
543
|
+
this.touch = output();
|
|
544
|
+
/**
|
|
545
|
+
* @description Sets the touched state of the OTP input. When bound to a signal form field via `[formField]`,
|
|
546
|
+
* this is written by the `FormField` directive.
|
|
547
|
+
* @default false
|
|
548
|
+
*/
|
|
549
|
+
this.touched = input(false, /* @ts-ignore */
|
|
550
|
+
...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
551
|
+
/**
|
|
552
|
+
* @description Sets the type of the OTP input ("text", "number", or "password").
|
|
553
|
+
* @default "text"
|
|
554
|
+
*/
|
|
555
|
+
this.type = input("text", /* @ts-ignore */
|
|
556
|
+
...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
|
|
557
|
+
/**
|
|
558
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
559
|
+
* @default ""
|
|
560
|
+
*/
|
|
561
|
+
this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class" });
|
|
562
|
+
/**
|
|
563
|
+
* @description Two-way bindable current value of the OTP input.
|
|
564
|
+
* @default ""
|
|
565
|
+
*/
|
|
566
|
+
this.value = model("", /* @ts-ignore */
|
|
567
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
568
|
+
effect(() => {
|
|
569
|
+
const val = this.value();
|
|
570
|
+
const inputType = this.type();
|
|
571
|
+
const characterPattern = this.characterPattern();
|
|
572
|
+
const maxLen = this.normalizedLength();
|
|
573
|
+
const filtered = filterCharacters(val, inputType, characterPattern, maxLen);
|
|
574
|
+
if (filtered !== val) {
|
|
575
|
+
this.value.set(filtered);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
blur() {
|
|
580
|
+
this.inputRef().nativeElement.blur();
|
|
581
|
+
}
|
|
582
|
+
focus(target) {
|
|
583
|
+
if (this.disabled()) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const el = this.inputRef().nativeElement;
|
|
587
|
+
if (typeof target === "object" && target !== null) {
|
|
588
|
+
el.focus(target);
|
|
589
|
+
this.syncSelection();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
el.focus();
|
|
593
|
+
const val = this.value();
|
|
594
|
+
const maxLen = this.normalizedLength();
|
|
595
|
+
if (typeof target === "number") {
|
|
596
|
+
const clamped = Math.max(0, Math.min(target, maxLen));
|
|
597
|
+
if (clamped < val.length) {
|
|
598
|
+
el.setSelectionRange(clamped, clamped + 1);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
const pos = Math.min(clamped, val.length);
|
|
602
|
+
el.setSelectionRange(pos, pos);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
if (val.length < maxLen) {
|
|
607
|
+
el.setSelectionRange(val.length, val.length);
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
el.setSelectionRange(maxLen, maxLen);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
this.syncSelection();
|
|
614
|
+
}
|
|
615
|
+
onBeforeInput(event) {
|
|
616
|
+
if (this.disabled() || this.readonly()) {
|
|
617
|
+
event.preventDefault();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (this.isComposing() || event.isComposing) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const inputType = event.inputType;
|
|
624
|
+
const inputEl = this.inputRef().nativeElement;
|
|
625
|
+
const start = inputEl.selectionStart ?? 0;
|
|
626
|
+
const end = inputEl.selectionEnd ?? 0;
|
|
627
|
+
const val = this.value();
|
|
628
|
+
const maxLen = this.normalizedLength();
|
|
629
|
+
if (inputType === "insertText") {
|
|
630
|
+
if (!event.data) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (event.data.length === 1) {
|
|
634
|
+
if (!isValidCharacter(event.data, this.type(), this.characterPattern())) {
|
|
635
|
+
event.preventDefault();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (start >= maxLen && start === end) {
|
|
639
|
+
event.preventDefault();
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (start === end && start < val.length) {
|
|
643
|
+
inputEl.setSelectionRange(start, start + 1);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
event.preventDefault();
|
|
648
|
+
this.applyUserInsertion(event.data, start, end);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
else if (inputType === "insertReplacementText" || inputType === "insertFromPaste") {
|
|
652
|
+
if (event.data) {
|
|
653
|
+
event.preventDefault();
|
|
654
|
+
this.applyUserInsertion(event.data, start, end);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
onBlur(event) {
|
|
659
|
+
this.hasFocus.set(false);
|
|
660
|
+
this.inputBlur.emit(event);
|
|
661
|
+
this.touch.emit();
|
|
662
|
+
}
|
|
663
|
+
onCompositionEnd(event) {
|
|
664
|
+
this.isComposing.set(false);
|
|
665
|
+
this.onInput(event);
|
|
666
|
+
}
|
|
667
|
+
onCompositionStart() {
|
|
668
|
+
this.isComposing.set(true);
|
|
669
|
+
}
|
|
670
|
+
onFocus(event) {
|
|
671
|
+
this.hasFocus.set(true);
|
|
672
|
+
this.syncSelection();
|
|
673
|
+
this.inputFocus.emit(event);
|
|
674
|
+
}
|
|
675
|
+
onInput(_event) {
|
|
676
|
+
if (this.disabled() || this.readonly()) {
|
|
677
|
+
const inputEl = this.inputRef().nativeElement;
|
|
678
|
+
inputEl.value = this.value();
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (this.isComposing()) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const inputEl = this.inputRef().nativeElement;
|
|
685
|
+
const rawVal = inputEl.value;
|
|
686
|
+
const prevVal = this.value();
|
|
687
|
+
const nextVal = filterCharacters(rawVal, this.type(), this.characterPattern(), this.normalizedLength());
|
|
688
|
+
inputEl.value = nextVal;
|
|
689
|
+
const wasIncomplete = prevVal.length < this.normalizedLength();
|
|
690
|
+
this.value.set(nextVal);
|
|
691
|
+
this.touch.emit();
|
|
692
|
+
if (wasIncomplete && nextVal.length === this.normalizedLength()) {
|
|
693
|
+
this.complete.emit(nextVal);
|
|
694
|
+
}
|
|
695
|
+
this.syncSelection();
|
|
696
|
+
}
|
|
697
|
+
onKeyDown(event) {
|
|
698
|
+
if (this.disabled() || this.readonly() || this.isComposing() || event.isComposing) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (event.ctrlKey || event.metaKey || event.altKey) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (event.key.length !== 1) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const inputEl = this.inputRef().nativeElement;
|
|
708
|
+
const start = inputEl.selectionStart ?? 0;
|
|
709
|
+
const end = inputEl.selectionEnd ?? 0;
|
|
710
|
+
const val = this.value();
|
|
711
|
+
const maxLen = this.normalizedLength();
|
|
712
|
+
if (!isValidCharacter(event.key, this.type(), this.characterPattern())) {
|
|
713
|
+
event.preventDefault();
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (start >= maxLen && start === end) {
|
|
717
|
+
event.preventDefault();
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (start === end && start < val.length) {
|
|
721
|
+
inputEl.setSelectionRange(start, start + 1);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
onKeyUp() {
|
|
725
|
+
this.syncSelection();
|
|
726
|
+
}
|
|
727
|
+
onPaste(event) {
|
|
728
|
+
if (this.disabled() || this.readonly()) {
|
|
729
|
+
event.preventDefault();
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const pastedText = event.clipboardData?.getData("text") ?? "";
|
|
733
|
+
if (!pastedText) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
event.preventDefault();
|
|
737
|
+
const inputEl = this.inputRef().nativeElement;
|
|
738
|
+
const start = inputEl.selectionStart ?? 0;
|
|
739
|
+
const end = inputEl.selectionEnd ?? 0;
|
|
740
|
+
this.applyUserInsertion(pastedText, start, end);
|
|
741
|
+
}
|
|
742
|
+
onSelect() {
|
|
743
|
+
this.syncSelection();
|
|
744
|
+
}
|
|
745
|
+
onSlotPointerDown(event, index) {
|
|
746
|
+
if (this.disabled()) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
event.preventDefault();
|
|
750
|
+
this.focus(index);
|
|
751
|
+
}
|
|
752
|
+
applyUserInsertion(text, selectionStart, selectionEnd) {
|
|
753
|
+
if (this.disabled() || this.readonly()) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const inputEl = this.inputRef().nativeElement;
|
|
757
|
+
const currentVal = this.value();
|
|
758
|
+
const maxLen = this.normalizedLength();
|
|
759
|
+
const filtered = filterCharacters(text, this.type(), this.characterPattern(), Number.MAX_SAFE_INTEGER);
|
|
760
|
+
if (filtered.length === 0 && selectionStart === selectionEnd) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const start = Math.max(0, Math.min(selectionStart, currentVal.length));
|
|
764
|
+
const end = Math.max(start, Math.min(selectionEnd, currentVal.length));
|
|
765
|
+
const before = currentVal.substring(0, start);
|
|
766
|
+
const after = currentVal.substring(end);
|
|
767
|
+
let combined = before + filtered + after;
|
|
768
|
+
if (combined.length > maxLen) {
|
|
769
|
+
combined = combined.substring(0, maxLen);
|
|
770
|
+
}
|
|
771
|
+
const newCaretPos = Math.min(start + filtered.length, maxLen);
|
|
772
|
+
const wasIncomplete = currentVal.length < maxLen;
|
|
773
|
+
inputEl.value = combined;
|
|
774
|
+
this.value.set(combined);
|
|
775
|
+
inputEl.setSelectionRange(newCaretPos, newCaretPos);
|
|
776
|
+
this.selectionStart.set(newCaretPos);
|
|
777
|
+
this.selectionEnd.set(newCaretPos);
|
|
778
|
+
this.touch.emit();
|
|
779
|
+
if (wasIncomplete && combined.length === maxLen) {
|
|
780
|
+
this.complete.emit(combined);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
syncSelection() {
|
|
784
|
+
const el = this.inputRef().nativeElement;
|
|
785
|
+
this.selectionStart.set(el.selectionStart ?? 0);
|
|
786
|
+
this.selectionEnd.set(el.selectionEnd ?? 0);
|
|
787
|
+
}
|
|
788
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: OtpInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
789
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: OtpInputComponent, isStandalone: true, selector: "mona-otp-input", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, characterPattern: { classPropertyName: "characterPattern", publicName: "characterPattern", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, groupLength: { classPropertyName: "groupLength", publicName: "groupLength", isSignal: true, isRequired: false, transformFunction: null }, inputAttributes: { classPropertyName: "inputAttributes", publicName: "inputAttributes", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, length: { classPropertyName: "length", publicName: "length", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, separator: { classPropertyName: "separator", publicName: "separator", isSignal: true, isRequired: false, transformFunction: null }, separatorClass: { classPropertyName: "separatorClass", publicName: "separatorClass", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, slotClass: { classPropertyName: "slotClass", publicName: "slotClass", isSignal: true, isRequired: false, transformFunction: null }, spacing: { classPropertyName: "spacing", publicName: "spacing", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { complete: "complete", inputBlur: "inputBlur", inputFocus: "inputFocus", touch: "touch", value: "valueChange" }, host: { attributes: { "dir": "ltr" }, properties: { "attr.dir": "'ltr'", "attr.data-disabled": "disabled() || null", "attr.data-readonly": "readonly() || null", "attr.data-required": "required() || null", "attr.data-invalid": "invalidState() || null", "attr.data-complete": "isComplete() || null", "attr.data-focused": "hasFocus() || null", "class": "classes()" } }, queries: [{ propertyName: "separatorTemplate", first: true, predicate: OtpInputSeparatorTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["input"], descendants: true, isSignal: true }], ngImport: i0, template: "<input\n #input\n [class]=\"inputClass()\"\n tabindex=\"0\"\n [value]=\"value()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [required]=\"required()\"\n [type]=\"nativeInputType()\"\n [attr.inputmode]=\"derivedInputMode()\"\n [attr.autocomplete]=\"derivedAutocomplete()\"\n [attr.autocapitalize]=\"'off'\"\n [attr.spellcheck]=\"'false'\"\n [attr.aria-label]=\"computedAriaLabel()\"\n [attr.aria-labelledby]=\"computedAriaLabelledby()\"\n [attr.aria-invalid]=\"invalidState() ? 'true' : null\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.maxlength]=\"normalizedLength()\"\n [monaAttributeBinder]=\"sanitizedAttributes()\"\n (beforeinput)=\"onBeforeInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (input)=\"onInput($event)\"\n (paste)=\"onPaste($event)\"\n (focus)=\"onFocus($event)\"\n (blur)=\"onBlur($event)\"\n (select)=\"onSelect()\"\n (keyup)=\"onKeyUp()\"\n (compositionstart)=\"onCompositionStart()\"\n (compositionend)=\"onCompositionEnd($event)\" />\n\n@for (group of visualGroups(); track group.index; let lastGroup = $last) {\n <div\n class=\"inline-flex items-center\"\n [class.gap-1.5]=\"spacing()\"\n [class.-space-x-px]=\"!spacing()\"\n [attr.data-group-index]=\"group.index\">\n @for (slot of group.slots; track slot.index) {\n <span\n class=\"otp-slot\"\n [class]=\"slot.classes\"\n [attr.data-slot-index]=\"slot.index\"\n [attr.data-active]=\"slot.active || null\"\n [attr.data-selected]=\"slot.selected || null\"\n [attr.data-filled]=\"slot.filled || null\"\n [attr.data-placeholder]=\"slot.placeholder || null\"\n [attr.data-disabled]=\"disabled() || null\"\n [attr.data-readonly]=\"readonly() || null\"\n [attr.data-invalid]=\"invalidState() || null\"\n (pointerdown)=\"onSlotPointerDown($event, slot.index)\"\n aria-hidden=\"true\">\n {{ slot.displayCharacter }}\n @if (slot.active && !slot.filled && hasFocus() && !disabled() && !readonly()) {\n <span\n class=\"pointer-events-none absolute inset-y-2 w-0.5 animate-pulse motion-reduce:animate-none bg-foreground\"\n aria-hidden=\"true\"></span>\n }\n </span>\n }\n </div>\n\n @if (!lastGroup && (separator() || separatorTemplate())) {\n <span\n class=\"inline-flex items-center justify-center text-muted-foreground select-none px-1\"\n [class]=\"separatorClass()\"\n aria-hidden=\"true\">\n @if (separatorTemplate(); as customTemplate) {\n <ng-container\n *ngTemplateOutlet=\"\n customTemplate;\n context: { $implicit: group.index, groupIndex: group.index }\n \"></ng-container>\n } @else {\n {{ separator() }}\n }\n </span>\n }\n}\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: AttributeBinderDirective, selector: "[monaAttributeBinder]", inputs: ["monaAttributeBinder"] }] }); }
|
|
790
|
+
}
|
|
791
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: OtpInputComponent, decorators: [{
|
|
792
|
+
type: Component,
|
|
793
|
+
args: [{ selector: "mona-otp-input", imports: [NgTemplateOutlet, AttributeBinderDirective], host: {
|
|
794
|
+
dir: "ltr",
|
|
795
|
+
"[attr.dir]": "'ltr'",
|
|
796
|
+
"[attr.data-disabled]": "disabled() || null",
|
|
797
|
+
"[attr.data-readonly]": "readonly() || null",
|
|
798
|
+
"[attr.data-required]": "required() || null",
|
|
799
|
+
"[attr.data-invalid]": "invalidState() || null",
|
|
800
|
+
"[attr.data-complete]": "isComplete() || null",
|
|
801
|
+
"[attr.data-focused]": "hasFocus() || null",
|
|
802
|
+
"[class]": "classes()"
|
|
803
|
+
}, template: "<input\n #input\n [class]=\"inputClass()\"\n tabindex=\"0\"\n [value]=\"value()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [required]=\"required()\"\n [type]=\"nativeInputType()\"\n [attr.inputmode]=\"derivedInputMode()\"\n [attr.autocomplete]=\"derivedAutocomplete()\"\n [attr.autocapitalize]=\"'off'\"\n [attr.spellcheck]=\"'false'\"\n [attr.aria-label]=\"computedAriaLabel()\"\n [attr.aria-labelledby]=\"computedAriaLabelledby()\"\n [attr.aria-invalid]=\"invalidState() ? 'true' : null\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.maxlength]=\"normalizedLength()\"\n [monaAttributeBinder]=\"sanitizedAttributes()\"\n (beforeinput)=\"onBeforeInput($event)\"\n (keydown)=\"onKeyDown($event)\"\n (input)=\"onInput($event)\"\n (paste)=\"onPaste($event)\"\n (focus)=\"onFocus($event)\"\n (blur)=\"onBlur($event)\"\n (select)=\"onSelect()\"\n (keyup)=\"onKeyUp()\"\n (compositionstart)=\"onCompositionStart()\"\n (compositionend)=\"onCompositionEnd($event)\" />\n\n@for (group of visualGroups(); track group.index; let lastGroup = $last) {\n <div\n class=\"inline-flex items-center\"\n [class.gap-1.5]=\"spacing()\"\n [class.-space-x-px]=\"!spacing()\"\n [attr.data-group-index]=\"group.index\">\n @for (slot of group.slots; track slot.index) {\n <span\n class=\"otp-slot\"\n [class]=\"slot.classes\"\n [attr.data-slot-index]=\"slot.index\"\n [attr.data-active]=\"slot.active || null\"\n [attr.data-selected]=\"slot.selected || null\"\n [attr.data-filled]=\"slot.filled || null\"\n [attr.data-placeholder]=\"slot.placeholder || null\"\n [attr.data-disabled]=\"disabled() || null\"\n [attr.data-readonly]=\"readonly() || null\"\n [attr.data-invalid]=\"invalidState() || null\"\n (pointerdown)=\"onSlotPointerDown($event, slot.index)\"\n aria-hidden=\"true\">\n {{ slot.displayCharacter }}\n @if (slot.active && !slot.filled && hasFocus() && !disabled() && !readonly()) {\n <span\n class=\"pointer-events-none absolute inset-y-2 w-0.5 animate-pulse motion-reduce:animate-none bg-foreground\"\n aria-hidden=\"true\"></span>\n }\n </span>\n }\n </div>\n\n @if (!lastGroup && (separator() || separatorTemplate())) {\n <span\n class=\"inline-flex items-center justify-center text-muted-foreground select-none px-1\"\n [class]=\"separatorClass()\"\n aria-hidden=\"true\">\n @if (separatorTemplate(); as customTemplate) {\n <ng-container\n *ngTemplateOutlet=\"\n customTemplate;\n context: { $implicit: group.index, groupIndex: group.index }\n \"></ng-container>\n } @else {\n {{ separator() }}\n }\n </span>\n }\n}\n" }]
|
|
804
|
+
}], ctorParameters: () => [], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ["input", { isSignal: true }] }], separatorTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OtpInputSeparatorTemplateDirective), { ...{
|
|
805
|
+
read: TemplateRef
|
|
806
|
+
}, isSignal: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], characterPattern: [{ type: i0.Input, args: [{ isSignal: true, alias: "characterPattern", required: false }] }], complete: [{ type: i0.Output, args: ["complete"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], groupLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupLength", required: false }] }], inputAttributes: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputAttributes", required: false }] }], inputBlur: [{ type: i0.Output, args: ["inputBlur"] }], inputFocus: [{ type: i0.Output, args: ["inputFocus"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], length: [{ type: i0.Input, args: [{ isSignal: true, alias: "length", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], separator: [{ type: i0.Input, args: [{ isSignal: true, alias: "separator", required: false }] }], separatorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorClass", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], slotClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "slotClass", required: false }] }], spacing: [{ type: i0.Input, args: [{ isSignal: true, alias: "spacing", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
|
|
807
|
+
|
|
808
|
+
/*
|
|
809
|
+
* Public API Surface of @nanahoshi/mona-ui/otp-input
|
|
810
|
+
*/
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Generated bundle index. Do not edit.
|
|
814
|
+
*/
|
|
815
|
+
|
|
816
|
+
export { OtpInputComponent, OtpInputSeparatorTemplateDirective };
|
|
817
|
+
//# sourceMappingURL=nanahoshi-mona-ui-otp-input.mjs.map
|
|
@@ -132,6 +132,7 @@ class SegmentedComponent {
|
|
|
132
132
|
#lastAnimatedIndex;
|
|
133
133
|
#lastOptions;
|
|
134
134
|
#lastSize;
|
|
135
|
+
#observedOptionElements;
|
|
135
136
|
#resizeObserver;
|
|
136
137
|
constructor() {
|
|
137
138
|
this.#destroyRef = inject(DestroyRef);
|
|
@@ -139,6 +140,7 @@ class SegmentedComponent {
|
|
|
139
140
|
this.#lastAnimatedIndex = null;
|
|
140
141
|
this.#lastOptions = null;
|
|
141
142
|
this.#lastSize = null;
|
|
143
|
+
this.#observedOptionElements = [];
|
|
142
144
|
this.#resizeObserver = null;
|
|
143
145
|
this.containerClasses = computed(() => {
|
|
144
146
|
const classes = segmentedContainerThemeVariants({ alignment: this.alignment(), rounded: this.rounded() });
|
|
@@ -265,7 +267,7 @@ class SegmentedComponent {
|
|
|
265
267
|
afterRenderEffect({
|
|
266
268
|
read: () => {
|
|
267
269
|
this.selectedIndex();
|
|
268
|
-
this.optionElements();
|
|
270
|
+
const optionElements = this.optionElements();
|
|
269
271
|
this.alignment();
|
|
270
272
|
this.rounded();
|
|
271
273
|
this.animate();
|
|
@@ -276,12 +278,14 @@ class SegmentedComponent {
|
|
|
276
278
|
const suppressTransition = isSizeChange || isOptionsChange;
|
|
277
279
|
this.#lastSize = size;
|
|
278
280
|
this.#lastOptions = options;
|
|
281
|
+
this.observeOptionElements(optionElements);
|
|
279
282
|
this.updateIndicatorGeometry({ suppressTransition });
|
|
280
283
|
}
|
|
281
284
|
});
|
|
282
285
|
afterNextRender({
|
|
283
286
|
read: () => {
|
|
284
287
|
this.setupResizeObserver();
|
|
288
|
+
this.scheduleSettledGeometryUpdate();
|
|
285
289
|
}
|
|
286
290
|
});
|
|
287
291
|
this.#destroyRef.onDestroy(() => {
|
|
@@ -307,6 +311,39 @@ class SegmentedComponent {
|
|
|
307
311
|
this.value.set(option.value);
|
|
308
312
|
this.touch.emit();
|
|
309
313
|
}
|
|
314
|
+
observeOptionElements(optionElements) {
|
|
315
|
+
if (!this.#resizeObserver) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const elements = optionElements.map(optionElement => optionElement.nativeElement);
|
|
319
|
+
for (const element of this.#observedOptionElements) {
|
|
320
|
+
if (!elements.includes(element)) {
|
|
321
|
+
this.#resizeObserver.unobserve(element);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const element of elements) {
|
|
325
|
+
if (!this.#observedOptionElements.includes(element)) {
|
|
326
|
+
this.#resizeObserver.observe(element);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
this.#observedOptionElements = elements;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Guards against a stale indicator snapshot when the control mounts inside a container that is
|
|
333
|
+
* still transitioning in (e.g. a popup's enter animation): the host's own box may not change once
|
|
334
|
+
* that settles, so the ResizeObserver on it alone would never re-fire to correct the geometry.
|
|
335
|
+
*/
|
|
336
|
+
scheduleSettledGeometryUpdate() {
|
|
337
|
+
const win = this.#hostElementRef.nativeElement.ownerDocument.defaultView;
|
|
338
|
+
if (!win) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
win.requestAnimationFrame(() => {
|
|
342
|
+
win.requestAnimationFrame(() => {
|
|
343
|
+
this.updateIndicatorGeometry({ suppressTransition: true });
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
}
|
|
310
347
|
setupResizeObserver() {
|
|
311
348
|
if (typeof ResizeObserver === "undefined") {
|
|
312
349
|
return;
|
|
@@ -315,6 +352,7 @@ class SegmentedComponent {
|
|
|
315
352
|
this.updateIndicatorGeometry({ suppressTransition: true });
|
|
316
353
|
});
|
|
317
354
|
this.#resizeObserver.observe(this.#hostElementRef.nativeElement);
|
|
355
|
+
this.observeOptionElements(this.optionElements());
|
|
318
356
|
}
|
|
319
357
|
updateIndicatorGeometry(options) {
|
|
320
358
|
const selectedIndex = this.selectedIndex();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanahoshi/mona-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "A very light Angular UI framework",
|
|
5
5
|
"author": "Nanahoshi",
|
|
6
6
|
"repository": {
|
|
@@ -239,6 +239,10 @@
|
|
|
239
239
|
"types": "./types/nanahoshi-mona-ui-numeric-text-box.d.ts",
|
|
240
240
|
"default": "./fesm2022/nanahoshi-mona-ui-numeric-text-box.mjs"
|
|
241
241
|
},
|
|
242
|
+
"./otp-input": {
|
|
243
|
+
"types": "./types/nanahoshi-mona-ui-otp-input.d.ts",
|
|
244
|
+
"default": "./fesm2022/nanahoshi-mona-ui-otp-input.mjs"
|
|
245
|
+
},
|
|
242
246
|
"./pager": {
|
|
243
247
|
"types": "./types/nanahoshi-mona-ui-pager.d.ts",
|
|
244
248
|
"default": "./fesm2022/nanahoshi-mona-ui-pager.mjs"
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import * as _angular_core from '@angular/core';
|
|
2
|
+
import { ElementRef, TemplateRef } from '@angular/core';
|
|
3
|
+
import { FormValueControl } from '@angular/forms/signals';
|
|
4
|
+
import { VariantInputs, AttributeConfig } from '@nanahoshi/mona-ui/internal';
|
|
5
|
+
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
6
|
+
import { VariantProps } from 'class-variance-authority';
|
|
7
|
+
|
|
8
|
+
type OtpInputType = "text" | "number" | "password";
|
|
9
|
+
|
|
10
|
+
declare const otpInputSlotThemeVariants: (props?: ({
|
|
11
|
+
rounded?: "full" | "large" | "medium" | "none" | "small" | null | undefined;
|
|
12
|
+
size?: "large" | "medium" | "small" | null | undefined;
|
|
13
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
14
|
+
type OtpInputSlotVariantProps = VariantProps<typeof otpInputSlotThemeVariants>;
|
|
15
|
+
type OtpInputSlotVariantInput = VariantInputs<OtpInputSlotVariantProps>;
|
|
16
|
+
type OtpInputVariantProps = OtpInputSlotVariantProps;
|
|
17
|
+
type OtpInputVariantInput = OtpInputSlotVariantInput;
|
|
18
|
+
|
|
19
|
+
interface OtpSlotViewModel {
|
|
20
|
+
active: boolean;
|
|
21
|
+
classes: string;
|
|
22
|
+
displayCharacter: string;
|
|
23
|
+
filled: boolean;
|
|
24
|
+
index: number;
|
|
25
|
+
placeholder: boolean;
|
|
26
|
+
selected: boolean;
|
|
27
|
+
}
|
|
28
|
+
interface OtpGroupViewModel {
|
|
29
|
+
index: number;
|
|
30
|
+
slots: OtpSlotViewModel[];
|
|
31
|
+
}
|
|
32
|
+
declare class OtpInputComponent implements OtpInputVariantInput, FormValueControl<string> {
|
|
33
|
+
protected readonly activeSlotIndex: _angular_core.Signal<number>;
|
|
34
|
+
protected readonly classes: _angular_core.Signal<string>;
|
|
35
|
+
protected readonly computedAriaLabel: _angular_core.Signal<string | null>;
|
|
36
|
+
protected readonly computedAriaLabelledby: _angular_core.Signal<string | null>;
|
|
37
|
+
protected readonly derivedAutocomplete: _angular_core.Signal<string>;
|
|
38
|
+
protected readonly derivedInputMode: _angular_core.Signal<string>;
|
|
39
|
+
protected readonly hasFocus: _angular_core.WritableSignal<boolean>;
|
|
40
|
+
protected readonly inputClass: _angular_core.Signal<string>;
|
|
41
|
+
protected readonly inputRef: _angular_core.Signal<ElementRef<HTMLInputElement>>;
|
|
42
|
+
protected readonly invalidState: _angular_core.Signal<boolean>;
|
|
43
|
+
protected readonly isComplete: _angular_core.Signal<boolean>;
|
|
44
|
+
protected readonly isComposing: _angular_core.WritableSignal<boolean>;
|
|
45
|
+
protected readonly logicalSelectionEnd: _angular_core.Signal<number>;
|
|
46
|
+
protected readonly logicalSelectionStart: _angular_core.Signal<number>;
|
|
47
|
+
protected readonly nativeInputType: _angular_core.Signal<"text" | "password">;
|
|
48
|
+
protected readonly normalizedLength: _angular_core.Signal<number>;
|
|
49
|
+
protected readonly sanitizedAttributes: _angular_core.Signal<AttributeConfig>;
|
|
50
|
+
protected readonly selectionEnd: _angular_core.WritableSignal<number>;
|
|
51
|
+
protected readonly selectionStart: _angular_core.WritableSignal<number>;
|
|
52
|
+
protected readonly separatorTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
|
|
53
|
+
protected readonly visualGroups: _angular_core.Signal<OtpGroupViewModel[]>;
|
|
54
|
+
/**
|
|
55
|
+
* @description Accessible name for the inner `<input>` element.
|
|
56
|
+
* @default "Verification code"
|
|
57
|
+
*/
|
|
58
|
+
readonly ariaLabel: _angular_core.InputSignal<string>;
|
|
59
|
+
/**
|
|
60
|
+
* @description Regular expression used to determine whether each individual OTP character is allowed.
|
|
61
|
+
* @default null
|
|
62
|
+
*/
|
|
63
|
+
readonly characterPattern: _angular_core.InputSignal<RegExp | null>;
|
|
64
|
+
/**
|
|
65
|
+
* @description Emitted when a user interaction completes the verification code.
|
|
66
|
+
*/
|
|
67
|
+
readonly complete: _angular_core.OutputEmitterRef<string>;
|
|
68
|
+
/**
|
|
69
|
+
* @description Renders the component with reduced visual emphasis and removes pointer interaction.
|
|
70
|
+
* When bound to a signal form field via `[formField]`, this is written by the `FormField` directive.
|
|
71
|
+
* @default false
|
|
72
|
+
*/
|
|
73
|
+
readonly disabled: _angular_core.InputSignal<boolean>;
|
|
74
|
+
/**
|
|
75
|
+
* @description Configures slot grouping lengths. Accepts a uniform number or an array of group sizes.
|
|
76
|
+
* @default null
|
|
77
|
+
*/
|
|
78
|
+
readonly groupLength: _angular_core.InputSignal<number | number[] | null>;
|
|
79
|
+
/**
|
|
80
|
+
* @description Additional HTML attributes applied directly to the inner `<input>` element.
|
|
81
|
+
* @default {}
|
|
82
|
+
*/
|
|
83
|
+
readonly inputAttributes: _angular_core.InputSignal<AttributeConfig>;
|
|
84
|
+
/**
|
|
85
|
+
* @description Emitted when the inner input loses focus.
|
|
86
|
+
*/
|
|
87
|
+
readonly inputBlur: _angular_core.OutputEmitterRef<FocusEvent>;
|
|
88
|
+
/**
|
|
89
|
+
* @description Emitted when the inner input gains focus.
|
|
90
|
+
*/
|
|
91
|
+
readonly inputFocus: _angular_core.OutputEmitterRef<FocusEvent>;
|
|
92
|
+
/**
|
|
93
|
+
* @description Marks the OTP input as invalid. When bound to a signal form field via `[formField]`,
|
|
94
|
+
* this is written by the `FormField` directive.
|
|
95
|
+
* @default false
|
|
96
|
+
*/
|
|
97
|
+
readonly invalid: _angular_core.InputSignal<boolean>;
|
|
98
|
+
/**
|
|
99
|
+
* @description Sets the total number of characters accepted by the OTP input and the number of visual slots.
|
|
100
|
+
* @default 4
|
|
101
|
+
*/
|
|
102
|
+
readonly length: _angular_core.InputSignal<number>;
|
|
103
|
+
/**
|
|
104
|
+
* @description Hint character displayed in empty visual slots.
|
|
105
|
+
* @default ""
|
|
106
|
+
*/
|
|
107
|
+
readonly placeholder: _angular_core.InputSignal<string>;
|
|
108
|
+
/**
|
|
109
|
+
* @description Prevents value changes while preserving the component's focusability and visual state.
|
|
110
|
+
* When bound to a signal form field via `[formField]`, this is written by the `FormField` directive.
|
|
111
|
+
* @default false
|
|
112
|
+
*/
|
|
113
|
+
readonly readonly: _angular_core.InputSignal<boolean>;
|
|
114
|
+
/**
|
|
115
|
+
* @description Marks the OTP input as required. Triggers invalid state when touched and incomplete.
|
|
116
|
+
* When bound to a signal form field via `[formField]`, this is written by the `FormField` directive.
|
|
117
|
+
* @default false
|
|
118
|
+
*/
|
|
119
|
+
readonly required: _angular_core.InputSignal<boolean>;
|
|
120
|
+
/**
|
|
121
|
+
* @description Border-radius preset applied to the OTP slots.
|
|
122
|
+
* @default "medium"
|
|
123
|
+
*/
|
|
124
|
+
readonly rounded: _angular_core.InputSignal<"full" | "large" | "medium" | "none" | "small" | null | undefined>;
|
|
125
|
+
/**
|
|
126
|
+
* @description Text character or string displayed between visual slot groups.
|
|
127
|
+
* @default ""
|
|
128
|
+
*/
|
|
129
|
+
readonly separator: _angular_core.InputSignal<string>;
|
|
130
|
+
/**
|
|
131
|
+
* @description Additional CSS classes applied to separator elements.
|
|
132
|
+
* @default ""
|
|
133
|
+
*/
|
|
134
|
+
readonly separatorClass: _angular_core.InputSignal<string | string[]>;
|
|
135
|
+
/**
|
|
136
|
+
* @description Size preset controlling the dimensions and font size of the OTP slots.
|
|
137
|
+
* @default "medium"
|
|
138
|
+
*/
|
|
139
|
+
readonly size: _angular_core.InputSignal<"large" | "medium" | "small" | null | undefined>;
|
|
140
|
+
/**
|
|
141
|
+
* @description Additional CSS classes applied to each visual slot element.
|
|
142
|
+
* @default ""
|
|
143
|
+
*/
|
|
144
|
+
readonly slotClass: _angular_core.InputSignal<string | string[]>;
|
|
145
|
+
/**
|
|
146
|
+
* @description Determines whether visual slots within a group have spacing between them or are joined together.
|
|
147
|
+
* @default true
|
|
148
|
+
*/
|
|
149
|
+
readonly spacing: _angular_core.InputSignal<boolean>;
|
|
150
|
+
/**
|
|
151
|
+
* @description Emitted when the OTP input is interacted with — on blur or user value change.
|
|
152
|
+
* The `FormField` directive listens to this to mark the field as touched.
|
|
153
|
+
*/
|
|
154
|
+
readonly touch: _angular_core.OutputEmitterRef<void>;
|
|
155
|
+
/**
|
|
156
|
+
* @description Sets the touched state of the OTP input. When bound to a signal form field via `[formField]`,
|
|
157
|
+
* this is written by the `FormField` directive.
|
|
158
|
+
* @default false
|
|
159
|
+
*/
|
|
160
|
+
readonly touched: _angular_core.InputSignal<boolean>;
|
|
161
|
+
/**
|
|
162
|
+
* @description Sets the type of the OTP input ("text", "number", or "password").
|
|
163
|
+
* @default "text"
|
|
164
|
+
*/
|
|
165
|
+
readonly type: _angular_core.InputSignal<OtpInputType>;
|
|
166
|
+
/**
|
|
167
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
168
|
+
* @default ""
|
|
169
|
+
*/
|
|
170
|
+
readonly userClass: _angular_core.InputSignal<string>;
|
|
171
|
+
/**
|
|
172
|
+
* @description Two-way bindable current value of the OTP input.
|
|
173
|
+
* @default ""
|
|
174
|
+
*/
|
|
175
|
+
readonly value: _angular_core.ModelSignal<string>;
|
|
176
|
+
constructor();
|
|
177
|
+
blur(): void;
|
|
178
|
+
focus(options?: FocusOptions): void;
|
|
179
|
+
focus(index?: number): void;
|
|
180
|
+
protected onBeforeInput(event: InputEvent): void;
|
|
181
|
+
protected onBlur(event: FocusEvent): void;
|
|
182
|
+
protected onCompositionEnd(event: CompositionEvent): void;
|
|
183
|
+
protected onCompositionStart(): void;
|
|
184
|
+
protected onFocus(event: FocusEvent): void;
|
|
185
|
+
protected onInput(_event?: Event): void;
|
|
186
|
+
protected onKeyDown(event: KeyboardEvent): void;
|
|
187
|
+
protected onKeyUp(): void;
|
|
188
|
+
protected onPaste(event: ClipboardEvent): void;
|
|
189
|
+
protected onSelect(): void;
|
|
190
|
+
protected onSlotPointerDown(event: PointerEvent, index: number): void;
|
|
191
|
+
private applyUserInsertion;
|
|
192
|
+
private syncSelection;
|
|
193
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<OtpInputComponent, never>;
|
|
194
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<OtpInputComponent, "mona-otp-input", never, { "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "characterPattern": { "alias": "characterPattern"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "groupLength": { "alias": "groupLength"; "required": false; "isSignal": true; }; "inputAttributes": { "alias": "inputAttributes"; "required": false; "isSignal": true; }; "invalid": { "alias": "invalid"; "required": false; "isSignal": true; }; "length": { "alias": "length"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "rounded": { "alias": "rounded"; "required": false; "isSignal": true; }; "separator": { "alias": "separator"; "required": false; "isSignal": true; }; "separatorClass": { "alias": "separatorClass"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "slotClass": { "alias": "slotClass"; "required": false; "isSignal": true; }; "spacing": { "alias": "spacing"; "required": false; "isSignal": true; }; "touched": { "alias": "touched"; "required": false; "isSignal": true; }; "type": { "alias": "type"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "complete": "complete"; "inputBlur": "inputBlur"; "inputFocus": "inputFocus"; "touch": "touch"; "value": "valueChange"; }, ["separatorTemplate"], never, true, never>;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface OtpInputSeparatorContext {
|
|
198
|
+
$implicit: number;
|
|
199
|
+
groupIndex: number;
|
|
200
|
+
}
|
|
201
|
+
declare class OtpInputSeparatorTemplateDirective {
|
|
202
|
+
readonly templateRef: TemplateRef<any>;
|
|
203
|
+
static ngTemplateContextGuard(_directive: OtpInputSeparatorTemplateDirective, _context: unknown): _context is OtpInputSeparatorContext;
|
|
204
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<OtpInputSeparatorTemplateDirective, never>;
|
|
205
|
+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<OtpInputSeparatorTemplateDirective, "ng-template[monaOtpInputSeparatorTemplate]", never, {}, {}, never, never, true, never>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export { OtpInputComponent, OtpInputSeparatorTemplateDirective };
|
|
209
|
+
export type { OtpInputSeparatorContext, OtpInputType, OtpInputVariantProps };
|
|
@@ -148,6 +148,13 @@ declare class SegmentedComponent<T extends SegmentedValue = SegmentedValue> impl
|
|
|
148
148
|
protected createOptionContext(option: SegmentedOption<T>, index: number): SegmentedItemTemplateContext<T>;
|
|
149
149
|
protected onOptionBlur(): void;
|
|
150
150
|
protected onOptionChange(option: SegmentedOption<T>): void;
|
|
151
|
+
private observeOptionElements;
|
|
152
|
+
/**
|
|
153
|
+
* Guards against a stale indicator snapshot when the control mounts inside a container that is
|
|
154
|
+
* still transitioning in (e.g. a popup's enter animation): the host's own box may not change once
|
|
155
|
+
* that settles, so the ResizeObserver on it alone would never re-fire to correct the geometry.
|
|
156
|
+
*/
|
|
157
|
+
private scheduleSettledGeometryUpdate;
|
|
151
158
|
private setupResizeObserver;
|
|
152
159
|
private updateIndicatorGeometry;
|
|
153
160
|
static ɵfac: _angular_core.ɵɵFactoryDeclaration<SegmentedComponent<any>, never>;
|