@authhero/widget 0.37.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authhero-widget/authhero-widget.esm.js +1 -1
- package/dist/authhero-widget/index.esm.js +1 -1
- package/dist/authhero-widget/p-b5f14fa6.entry.js +1 -0
- package/dist/authhero-widget/p-f94037cd.entry.js +1 -0
- package/dist/cjs/authhero-node.cjs.entry.js +458 -13
- package/dist/cjs/authhero-widget.cjs.entry.js +79 -10
- package/dist/cjs/authhero-widget.cjs.js +1 -1
- package/dist/cjs/index.cjs.js +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/collection/components/authhero-node/authhero-node.css +94 -0
- package/dist/collection/components/authhero-node/authhero-node.js +302 -13
- package/dist/collection/components/authhero-widget/authhero-widget.js +98 -10
- package/dist/collection/utils/date-format.js +176 -0
- package/dist/components/authhero-node.js +1 -1
- package/dist/components/authhero-widget.js +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/p-C9ZfIQiS.js +1 -0
- package/dist/esm/authhero-node.entry.js +458 -13
- package/dist/esm/authhero-widget.entry.js +79 -10
- package/dist/esm/authhero-widget.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/types/components/authhero-node/authhero-node.d.ts +83 -0
- package/dist/types/components/authhero-widget/authhero-widget.d.ts +19 -0
- package/dist/types/components.d.ts +18 -0
- package/dist/types/utils/date-format.d.ts +52 -0
- package/hydrate/index.js +541 -24
- package/hydrate/index.mjs +541 -24
- package/package.json +2 -2
- package/dist/authhero-widget/p-cdfc4555.entry.js +0 -1
- package/dist/authhero-widget/p-e95c436f.entry.js +0 -1
- package/dist/components/p-BP46GHTc.js +0 -1
|
@@ -369,6 +369,100 @@ input[type="date"].input-field+.input-label {
|
|
|
369
369
|
left: 8px;
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
/* ==========================================================================
|
|
373
|
+
Date Input - Segmented day / month / year
|
|
374
|
+
Part: date-input, date-segment, date-separator
|
|
375
|
+
|
|
376
|
+
The group is styled as one field: the outer box carries the border and
|
|
377
|
+
focus ring, the segments inside are borderless so three inputs read as a
|
|
378
|
+
single control.
|
|
379
|
+
========================================================================== */
|
|
380
|
+
|
|
381
|
+
.date-input {
|
|
382
|
+
display: flex;
|
|
383
|
+
align-items: center;
|
|
384
|
+
width: 100%;
|
|
385
|
+
padding: 16px;
|
|
386
|
+
box-sizing: border-box;
|
|
387
|
+
background-color: var(--ah-color-input-bg, #ffffff);
|
|
388
|
+
border: var(--ah-input-border-width, 1px) solid
|
|
389
|
+
var(--ah-color-border, #c9cace);
|
|
390
|
+
border-radius: var(--ah-input-radius, 3px);
|
|
391
|
+
transition:
|
|
392
|
+
border-color 0.15s ease-out,
|
|
393
|
+
box-shadow 0.15s ease-out;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.date-input:hover {
|
|
397
|
+
border-color: var(--ah-color-border-hover, #65676e);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.date-input:focus-within {
|
|
401
|
+
border-color: var(--ah-color-primary, #635dff);
|
|
402
|
+
box-shadow: inset 0 0 0 1px var(--ah-color-primary, #635dff);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.date-input.has-error {
|
|
406
|
+
border-color: var(--ah-color-error, #d03c38);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.date-input.has-error:focus-within {
|
|
410
|
+
box-shadow: inset 0 0 0 1px var(--ah-color-error, #d03c38);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* Forced-colors mode drops box-shadow, which is the only thing marking the
|
|
414
|
+
segment group as focused — the border colour is overridden too. Draw an
|
|
415
|
+
outline in a system colour so keyboard focus stays visible. */
|
|
416
|
+
@media (forced-colors: active) {
|
|
417
|
+
.date-input:focus-within,
|
|
418
|
+
.date-input.has-error:focus-within {
|
|
419
|
+
outline: 2px solid Highlight;
|
|
420
|
+
outline-offset: 1px;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.date-segment {
|
|
425
|
+
/* Sized in ch so the boxes track the font: wide enough for the placeholder
|
|
426
|
+
tokens (DD/MM are wider than "00") without leaving a gap around a typed
|
|
427
|
+
value. The year box holds twice the digits. */
|
|
428
|
+
width: 3.25ch;
|
|
429
|
+
min-width: 0;
|
|
430
|
+
padding: 0;
|
|
431
|
+
font-size: 16px;
|
|
432
|
+
font-family: inherit;
|
|
433
|
+
text-align: center;
|
|
434
|
+
color: var(--ah-color-input-text, var(--ah-color-text, #1e212a));
|
|
435
|
+
background: transparent;
|
|
436
|
+
border: none;
|
|
437
|
+
outline: none;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.date-segment[data-date-segment="year"] {
|
|
441
|
+
width: 5.5ch;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* Shown here, unlike other fields: the tokens are the field's format hint */
|
|
445
|
+
.date-segment::placeholder {
|
|
446
|
+
color: var(--ah-color-text-muted, #65676e);
|
|
447
|
+
opacity: 1;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.date-segment:disabled {
|
|
451
|
+
color: var(--ah-color-text-disabled, #a0a0a0);
|
|
452
|
+
cursor: not-allowed;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.date-input:has(.date-segment:disabled) {
|
|
456
|
+
background-color: var(--ah-color-bg-disabled, #f5f5f5);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.date-separator {
|
|
460
|
+
padding: 0 4px;
|
|
461
|
+
font-size: 16px;
|
|
462
|
+
color: var(--ah-color-text-muted, #65676e);
|
|
463
|
+
user-select: none;
|
|
464
|
+
}
|
|
465
|
+
|
|
372
466
|
/* Social buttons container */
|
|
373
467
|
.social-buttons {
|
|
374
468
|
display: flex;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { h, } from "@stencil/core";
|
|
2
2
|
import { countries, getCountryByCode, } from "../../utils/country-data";
|
|
3
|
+
import { expandTwoDigitYear, getDateLayout, parseIsoDate, resolveYearAnchor, toIsoDate, } from "../../utils/date-format";
|
|
3
4
|
export class AuthheroNode {
|
|
4
5
|
/**
|
|
5
6
|
* The component configuration to render.
|
|
@@ -15,6 +16,12 @@ export class AuthheroNode {
|
|
|
15
16
|
* Whether the component is disabled.
|
|
16
17
|
*/
|
|
17
18
|
disabled = false;
|
|
19
|
+
/**
|
|
20
|
+
* BCP-47 locale used for locale-dependent field layout (currently the
|
|
21
|
+
* segment order of DATE fields). Resolved server-side so SSR and hydration
|
|
22
|
+
* agree; falls back to day-month-year when absent.
|
|
23
|
+
*/
|
|
24
|
+
locale;
|
|
18
25
|
/**
|
|
19
26
|
* Whether the password field is visible.
|
|
20
27
|
*/
|
|
@@ -36,6 +43,15 @@ export class AuthheroNode {
|
|
|
36
43
|
* When true, the value is emitted as-is without dial code prefix.
|
|
37
44
|
*/
|
|
38
45
|
telEmailMode = false;
|
|
46
|
+
/**
|
|
47
|
+
* Segment values for the DATE input, each held as typed (unpadded while the
|
|
48
|
+
* user is mid-entry) and normalised on blur.
|
|
49
|
+
*/
|
|
50
|
+
dateSegments = {
|
|
51
|
+
year: "",
|
|
52
|
+
month: "",
|
|
53
|
+
day: "",
|
|
54
|
+
};
|
|
39
55
|
/**
|
|
40
56
|
* Emitted when a field value changes.
|
|
41
57
|
*/
|
|
@@ -47,13 +63,16 @@ export class AuthheroNode {
|
|
|
47
63
|
componentChanged() {
|
|
48
64
|
this.initCountryFromConfig();
|
|
49
65
|
this.initTelValue();
|
|
66
|
+
this.initDateValue();
|
|
50
67
|
}
|
|
51
68
|
valueChanged() {
|
|
52
69
|
this.initTelValue();
|
|
70
|
+
this.initDateValue();
|
|
53
71
|
}
|
|
54
72
|
componentWillLoad() {
|
|
55
73
|
this.initCountryFromConfig();
|
|
56
74
|
this.initTelValue();
|
|
75
|
+
this.initDateValue();
|
|
57
76
|
}
|
|
58
77
|
componentDidLoad() {
|
|
59
78
|
// Auto-focus a code field so the caret starts in the first (leftmost) box.
|
|
@@ -79,6 +98,20 @@ export class AuthheroNode {
|
|
|
79
98
|
}
|
|
80
99
|
}
|
|
81
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* The last value this field emitted for a TEL component. The widget mirrors
|
|
103
|
+
* every emitted value back onto the `value` prop, and that echo must not be
|
|
104
|
+
* re-parsed: the wire format carries only a dial code, so a country picked
|
|
105
|
+
* from the list would be replaced by the first entry sharing that code — a
|
|
106
|
+
* user who chose Canada would be silently moved to the US on their next
|
|
107
|
+
* keystroke.
|
|
108
|
+
*/
|
|
109
|
+
lastEmittedTel;
|
|
110
|
+
/** Emit a TEL value and remember it, so the echo can be recognised. */
|
|
111
|
+
emitTelValue(value) {
|
|
112
|
+
this.lastEmittedTel = value;
|
|
113
|
+
this.fieldChange.emit({ id: this.component.id, value });
|
|
114
|
+
}
|
|
82
115
|
/**
|
|
83
116
|
* Hydrate localPhoneNumber (and selectedCountry) from the effective value
|
|
84
117
|
* for TEL fields. The full value is stored as `{dialCode}{localNumber}`,
|
|
@@ -87,6 +120,10 @@ export class AuthheroNode {
|
|
|
87
120
|
initTelValue() {
|
|
88
121
|
if (this.component?.type !== "TEL")
|
|
89
122
|
return;
|
|
123
|
+
if (this.lastEmittedTel !== undefined &&
|
|
124
|
+
this.getEffectiveValue() === this.lastEmittedTel) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
90
127
|
const config = this.component.config;
|
|
91
128
|
const allowEmail = config?.allow_email === true;
|
|
92
129
|
const fullValue = this.getEffectiveValue();
|
|
@@ -121,11 +158,16 @@ export class AuthheroNode {
|
|
|
121
158
|
handleCountryChange = (e) => {
|
|
122
159
|
const target = e.target;
|
|
123
160
|
this.selectedCountry = getCountryByCode(target.value);
|
|
161
|
+
// A half-typed "+xx" prefix is replaced by the picked country, not kept
|
|
162
|
+
// as a local number (it would emit "+46+4").
|
|
163
|
+
if (this.localPhoneNumber.startsWith("+")) {
|
|
164
|
+
this.localPhoneNumber = "";
|
|
165
|
+
}
|
|
124
166
|
// Re-emit the full phone number with new dial code
|
|
125
167
|
const fullNumber = this.localPhoneNumber
|
|
126
168
|
? `${this.selectedCountry.dialCode}${this.localPhoneNumber}`
|
|
127
169
|
: "";
|
|
128
|
-
this.
|
|
170
|
+
this.emitTelValue(fullNumber);
|
|
129
171
|
};
|
|
130
172
|
/**
|
|
131
173
|
* Try to detect a dial code prefix in the raw input (e.g. "+46", "0046")
|
|
@@ -163,7 +205,21 @@ export class AuthheroNode {
|
|
|
163
205
|
target.value = cleanedLocal;
|
|
164
206
|
this.localPhoneNumber = cleanedLocal;
|
|
165
207
|
const fullNumber = `${this.selectedCountry.dialCode}${cleanedLocal}`;
|
|
166
|
-
this.
|
|
208
|
+
this.emitTelValue(fullNumber);
|
|
209
|
+
}
|
|
210
|
+
else if (value.startsWith("+")) {
|
|
211
|
+
// A leading "+" is an international prefix the user is still typing.
|
|
212
|
+
// Keep it in the field so the dial code can build up until it matches
|
|
213
|
+
// a country — stripping it per keystroke means "+46" never resolves to
|
|
214
|
+
// Sweden, since each character is discarded before the next arrives.
|
|
215
|
+
const cleaned = `+${value.slice(1).replace(/[^\d\s\-()]/g, "")}`;
|
|
216
|
+
if (cleaned !== value) {
|
|
217
|
+
target.value = cleaned;
|
|
218
|
+
}
|
|
219
|
+
this.localPhoneNumber = cleaned;
|
|
220
|
+
// Emit the partial prefix as-is; prefixing it with the (not yet chosen)
|
|
221
|
+
// country's dial code would produce nonsense like "+1+4".
|
|
222
|
+
this.emitTelValue(cleaned);
|
|
167
223
|
}
|
|
168
224
|
else {
|
|
169
225
|
const cleaned = allowPlus
|
|
@@ -176,7 +232,7 @@ export class AuthheroNode {
|
|
|
176
232
|
const fullNumber = cleaned
|
|
177
233
|
? `${this.selectedCountry.dialCode}${cleaned}`
|
|
178
234
|
: "";
|
|
179
|
-
this.
|
|
235
|
+
this.emitTelValue(fullNumber);
|
|
180
236
|
}
|
|
181
237
|
}
|
|
182
238
|
handlePhoneInput = (e) => {
|
|
@@ -196,7 +252,7 @@ export class AuthheroNode {
|
|
|
196
252
|
else {
|
|
197
253
|
// Email or text — emit as-is
|
|
198
254
|
this.localPhoneNumber = value;
|
|
199
|
-
this.
|
|
255
|
+
this.emitTelValue(value);
|
|
200
256
|
}
|
|
201
257
|
return;
|
|
202
258
|
}
|
|
@@ -207,6 +263,196 @@ export class AuthheroNode {
|
|
|
207
263
|
const target = e.target;
|
|
208
264
|
this.fieldChange.emit({ id: this.component.id, value: target.value });
|
|
209
265
|
};
|
|
266
|
+
// ===========================================================================
|
|
267
|
+
// DATE segment handling
|
|
268
|
+
// ===========================================================================
|
|
269
|
+
/**
|
|
270
|
+
* The last ISO value this field emitted. The widget mirrors every emitted
|
|
271
|
+
* value back onto the `value` prop, and that echo must not be treated as an
|
|
272
|
+
* external change: it would re-format the segments under the caret while
|
|
273
|
+
* the user is still typing (a day of "3" comes back as "03", swallowing the
|
|
274
|
+
* next digit).
|
|
275
|
+
*/
|
|
276
|
+
lastEmittedDate;
|
|
277
|
+
/** Hydrate the day/month/year segments from the effective ISO value. */
|
|
278
|
+
initDateValue() {
|
|
279
|
+
if (this.component?.type !== "DATE")
|
|
280
|
+
return;
|
|
281
|
+
if (this.lastEmittedDate !== undefined &&
|
|
282
|
+
this.getEffectiveValue() === this.lastEmittedDate) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const parsed = parseIsoDate(this.getEffectiveValue());
|
|
286
|
+
if (parsed) {
|
|
287
|
+
this.dateSegments = parsed;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// No parseable value. Only clear when we currently hold a complete date,
|
|
291
|
+
// i.e. this is a genuine external reset: a half-typed date emits "" and
|
|
292
|
+
// the parent echoes that straight back as the value prop, which would
|
|
293
|
+
// otherwise wipe the segment the user just typed into.
|
|
294
|
+
if (toIsoDate(this.dateSegments) !== "") {
|
|
295
|
+
this.dateSegments = { year: "", month: "", day: "" };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
dateConfig() {
|
|
299
|
+
return this.component.config;
|
|
300
|
+
}
|
|
301
|
+
getDateLayoutForField() {
|
|
302
|
+
const config = this.dateConfig();
|
|
303
|
+
return getDateLayout(config?.format, this.locale);
|
|
304
|
+
}
|
|
305
|
+
static SEGMENT_LENGTH = {
|
|
306
|
+
day: 2,
|
|
307
|
+
month: 2,
|
|
308
|
+
year: 4,
|
|
309
|
+
};
|
|
310
|
+
/** Birthdate autofill tokens — the common case for a typed date. */
|
|
311
|
+
static SEGMENT_AUTOCOMPLETE = {
|
|
312
|
+
day: "bday-day",
|
|
313
|
+
month: "bday-month",
|
|
314
|
+
year: "bday-year",
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* The submittable value for the current segments: an ISO date, or "" when
|
|
318
|
+
* it is incomplete, impossible, or outside the field's `min`/`max`. ISO
|
|
319
|
+
* strings compare chronologically, so the bounds need no parsing.
|
|
320
|
+
*/
|
|
321
|
+
dateIsoValue(segments) {
|
|
322
|
+
const iso = toIsoDate(segments);
|
|
323
|
+
if (!iso)
|
|
324
|
+
return "";
|
|
325
|
+
const config = this.dateConfig();
|
|
326
|
+
const min = config?.min;
|
|
327
|
+
const max = config?.max;
|
|
328
|
+
if (typeof min === "string" && parseIsoDate(min) && iso < min)
|
|
329
|
+
return "";
|
|
330
|
+
if (typeof max === "string" && parseIsoDate(max) && iso > max)
|
|
331
|
+
return "";
|
|
332
|
+
return iso;
|
|
333
|
+
}
|
|
334
|
+
setDateSegments(segments) {
|
|
335
|
+
this.dateSegments = segments;
|
|
336
|
+
this.lastEmittedDate = this.dateIsoValue(segments);
|
|
337
|
+
this.fieldChange.emit({
|
|
338
|
+
id: this.component.id,
|
|
339
|
+
value: this.lastEmittedDate,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
focusDateSegment(segment) {
|
|
343
|
+
const input = this.host.shadowRoot?.querySelector(`input[data-date-segment="${segment}"]`);
|
|
344
|
+
input?.focus();
|
|
345
|
+
input?.select();
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Whether typing this digit completes the segment. A segment is complete at
|
|
349
|
+
* its full width, and also as soon as a further digit could not produce a
|
|
350
|
+
* valid number — no day starts with 4 and no month starts with 2.
|
|
351
|
+
*/
|
|
352
|
+
isDateSegmentComplete(segment, value) {
|
|
353
|
+
if (value.length >= AuthheroNode.SEGMENT_LENGTH[segment])
|
|
354
|
+
return true;
|
|
355
|
+
if (value.length !== 1)
|
|
356
|
+
return false;
|
|
357
|
+
if (segment === "day")
|
|
358
|
+
return Number(value) > 3;
|
|
359
|
+
if (segment === "month")
|
|
360
|
+
return Number(value) > 1;
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
handleDateSegmentInput = (segment) => (e) => {
|
|
364
|
+
const target = e.target;
|
|
365
|
+
const digits = target.value
|
|
366
|
+
.replace(/\D/g, "")
|
|
367
|
+
.slice(0, AuthheroNode.SEGMENT_LENGTH[segment]);
|
|
368
|
+
if (digits !== target.value) {
|
|
369
|
+
target.value = digits;
|
|
370
|
+
}
|
|
371
|
+
this.setDateSegments({ ...this.dateSegments, [segment]: digits });
|
|
372
|
+
if (this.isDateSegmentComplete(segment, digits)) {
|
|
373
|
+
const order = this.getDateLayoutForField().order;
|
|
374
|
+
const next = order[order.indexOf(segment) + 1];
|
|
375
|
+
if (next)
|
|
376
|
+
this.focusDateSegment(next);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
/**
|
|
380
|
+
* Pad a single digit ("5" -> "05") and expand a two-digit year against the
|
|
381
|
+
* field's upper bound ("85" -> 1985). Shared by blur and paste so a pasted
|
|
382
|
+
* date is read the same way as a typed one.
|
|
383
|
+
*/
|
|
384
|
+
normalizeDateSegment(segment, value) {
|
|
385
|
+
if (segment !== "year")
|
|
386
|
+
return value.padStart(2, "0");
|
|
387
|
+
return expandTwoDigitYear(value, resolveYearAnchor(this.dateConfig()?.max, new Date()));
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Normalise on blur: pad a single digit ("5" -> "05") and expand a two-digit
|
|
391
|
+
* year against the field's upper bound ("85" -> 1985).
|
|
392
|
+
*/
|
|
393
|
+
handleDateSegmentBlur = (segment) => (e) => {
|
|
394
|
+
const target = e.target;
|
|
395
|
+
const value = target.value.replace(/\D/g, "");
|
|
396
|
+
if (!value)
|
|
397
|
+
return;
|
|
398
|
+
const normalized = this.normalizeDateSegment(segment, value);
|
|
399
|
+
if (normalized === value)
|
|
400
|
+
return;
|
|
401
|
+
target.value = normalized;
|
|
402
|
+
this.setDateSegments({ ...this.dateSegments, [segment]: normalized });
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* Backspace at the start of an empty segment moves to the previous one, so
|
|
406
|
+
* the whole date can be cleared without reaching for the mouse.
|
|
407
|
+
*/
|
|
408
|
+
handleDateSegmentKeyDown = (segment) => (e) => {
|
|
409
|
+
if (e.key === "Enter") {
|
|
410
|
+
e.preventDefault();
|
|
411
|
+
this.buttonClick.emit({ id: "submit", type: "submit", value: "next" });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (e.key !== "Backspace")
|
|
415
|
+
return;
|
|
416
|
+
const target = e.target;
|
|
417
|
+
if (target.value.length > 0 || target.selectionStart !== 0)
|
|
418
|
+
return;
|
|
419
|
+
const order = this.getDateLayoutForField().order;
|
|
420
|
+
const previous = order[order.indexOf(segment) - 1];
|
|
421
|
+
if (previous) {
|
|
422
|
+
e.preventDefault();
|
|
423
|
+
this.focusDateSegment(previous);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
/**
|
|
427
|
+
* Pasting a whole date into any segment fills all three. An ISO value is
|
|
428
|
+
* read year-first; anything else follows the field's displayed order. A
|
|
429
|
+
* two-digit year is expanded the same way a typed one is, so "15/03/85"
|
|
430
|
+
* pastes as 1985 rather than falling through to the browser's own paste.
|
|
431
|
+
*/
|
|
432
|
+
handleDatePaste = (e) => {
|
|
433
|
+
const text = e.clipboardData?.getData("text") ?? "";
|
|
434
|
+
const groups = text.match(/\d+/g);
|
|
435
|
+
if (!groups || groups.length < 3)
|
|
436
|
+
return;
|
|
437
|
+
const [first, second, third] = groups;
|
|
438
|
+
const isoLike = first.length === 4;
|
|
439
|
+
const order = isoLike
|
|
440
|
+
? ["year", "month", "day"]
|
|
441
|
+
: this.getDateLayoutForField().order;
|
|
442
|
+
const segments = {
|
|
443
|
+
year: "",
|
|
444
|
+
month: "",
|
|
445
|
+
day: "",
|
|
446
|
+
};
|
|
447
|
+
[first, second, third].forEach((group, index) => {
|
|
448
|
+
const segment = order[index];
|
|
449
|
+
segments[segment] = this.normalizeDateSegment(segment, group.slice(0, AuthheroNode.SEGMENT_LENGTH[segment]));
|
|
450
|
+
});
|
|
451
|
+
if (!toIsoDate(segments))
|
|
452
|
+
return;
|
|
453
|
+
e.preventDefault();
|
|
454
|
+
this.setDateSegments(segments);
|
|
455
|
+
};
|
|
210
456
|
handleKeyDown = (e) => {
|
|
211
457
|
if (e.key === "Enter") {
|
|
212
458
|
e.preventDefault();
|
|
@@ -219,7 +465,7 @@ export class AuthheroNode {
|
|
|
219
465
|
if (config?.allow_email === true && target.value.length === 0) {
|
|
220
466
|
this.telEmailMode = true;
|
|
221
467
|
this.localPhoneNumber = "";
|
|
222
|
-
this.
|
|
468
|
+
this.emitTelValue("");
|
|
223
469
|
}
|
|
224
470
|
}
|
|
225
471
|
};
|
|
@@ -494,17 +740,40 @@ export class AuthheroNode {
|
|
|
494
740
|
const effectiveValue = this.getEffectiveValue();
|
|
495
741
|
return (h("div", { class: "input-wrapper", part: "input-wrapper" }, this.renderLabel(component.label, inputId, component.required), h("input", { id: inputId, class: this.getInputFieldClass(errors.length > 0), part: "input", type: "url", name: component.id, value: effectiveValue ?? "", placeholder: component.config?.placeholder, required: component.required, disabled: this.disabled, onInput: this.handleInput, onKeyDown: this.handleKeyDown }), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
|
|
496
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* DATE is rendered as three numeric segments rather than `input type=date`.
|
|
745
|
+
* A native date input renders differently in every browser (Safari adds a
|
|
746
|
+
* stepper), keeps its dd/mm/yyyy hint visible while half-filled, and asks
|
|
747
|
+
* for a calendar gesture to reach a year decades back — all wrong for a
|
|
748
|
+
* date typed from memory, which is what these fields collect.
|
|
749
|
+
*/
|
|
497
750
|
renderDateField(component) {
|
|
498
|
-
const inputId = `input-${component.id}`;
|
|
499
751
|
const errors = this.getErrors();
|
|
500
|
-
const {
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
752
|
+
const { format } = component.config ?? {};
|
|
753
|
+
const layout = getDateLayout(format, this.locale);
|
|
754
|
+
const isoValue = this.dateIsoValue(this.dateSegments);
|
|
755
|
+
// Every segment filled but no ISO value means the date does not exist
|
|
756
|
+
// (31 February) or falls outside min/max. Flag it rather than silently
|
|
757
|
+
// submitting nothing — the widget posts JSON, so the browser's own
|
|
758
|
+
// constraint validation never runs.
|
|
759
|
+
const isIncomplete = layout.order.some((segment) => this.dateSegments[segment].length === 0);
|
|
760
|
+
const hasInvalidDate = !isoValue && !isIncomplete;
|
|
761
|
+
const hasError = errors.length > 0 || hasInvalidDate;
|
|
762
|
+
const firstInputId = `input-${component.id}`;
|
|
763
|
+
const segmentInput = (segment) => (h("input", { key: segment, id: segment === layout.order[0] ? firstInputId : undefined, class: "date-segment", part: `input date-segment date-segment-${segment}`, type: "text", inputmode: "numeric", autocomplete: AuthheroNode.SEGMENT_AUTOCOMPLETE[segment], "data-date-segment": segment, maxlength: AuthheroNode.SEGMENT_LENGTH[segment], size: AuthheroNode.SEGMENT_LENGTH[segment], value: this.dateSegments[segment], placeholder: layout.tokens[segment], "aria-label": layout.tokens[segment], "aria-invalid": hasError ? "true" : "false", required: component.required, disabled: this.disabled, onInput: this.handleDateSegmentInput(segment), onBlur: this.handleDateSegmentBlur(segment), onKeyDown: this.handleDateSegmentKeyDown(segment), onPaste: this.handleDatePaste }));
|
|
764
|
+
return (h("div", { class: "input-wrapper", part: "input-wrapper" }, h("div", { class: "input-container" }, h("div", { class: hasError ? "date-input has-error" : "date-input", part: "date-input", role: "group", "aria-label": component.label }, layout.order.map((segment, index) => [
|
|
765
|
+
index > 0 && (h("span", { key: `sep-${segment}`, class: "date-separator", part: "date-separator", "aria-hidden": "true" }, layout.separator)),
|
|
766
|
+
segmentInput(segment),
|
|
767
|
+
])), this.renderFloatingLabel(component.label, firstInputId, component.required, true)), h("input", { type: "hidden", name: component.id, "data-input-name": component.id, value: isoValue }), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
|
|
505
768
|
}
|
|
506
769
|
renderBooleanField(component) {
|
|
507
|
-
return (h("label", { class: "checkbox-wrapper", part: "checkbox-wrapper" }, h("input", { type: "checkbox", part: "checkbox", name: component.id, checked:
|
|
770
|
+
return (h("label", { class: "checkbox-wrapper", part: "checkbox-wrapper" }, h("input", { type: "checkbox", part: "checkbox", name: component.id, checked:
|
|
771
|
+
// Once the field has a value it is authoritative — falling back to
|
|
772
|
+
// the default here would keep a ticked-by-default box ticked after
|
|
773
|
+
// the user unticks it.
|
|
774
|
+
this.value === undefined
|
|
775
|
+
? component.config?.default_value === true
|
|
776
|
+
: this.value === "true", required: component.required, disabled: this.disabled, onChange: this.handleCheckbox }), h("span", { class: "checkbox-label", part: "checkbox-label" }, component.label)));
|
|
508
777
|
}
|
|
509
778
|
renderLegalField(component) {
|
|
510
779
|
const text = component.config?.text ?? component.label ?? "";
|
|
@@ -750,6 +1019,25 @@ export class AuthheroNode {
|
|
|
750
1019
|
"reflect": false,
|
|
751
1020
|
"attribute": "disabled",
|
|
752
1021
|
"defaultValue": "false"
|
|
1022
|
+
},
|
|
1023
|
+
"locale": {
|
|
1024
|
+
"type": "string",
|
|
1025
|
+
"mutable": false,
|
|
1026
|
+
"complexType": {
|
|
1027
|
+
"original": "string",
|
|
1028
|
+
"resolved": "string | undefined",
|
|
1029
|
+
"references": {}
|
|
1030
|
+
},
|
|
1031
|
+
"required": false,
|
|
1032
|
+
"optional": true,
|
|
1033
|
+
"docs": {
|
|
1034
|
+
"tags": [],
|
|
1035
|
+
"text": "BCP-47 locale used for locale-dependent field layout (currently the\nsegment order of DATE fields). Resolved server-side so SSR and hydration\nagree; falls back to day-month-year when absent."
|
|
1036
|
+
},
|
|
1037
|
+
"getter": false,
|
|
1038
|
+
"setter": false,
|
|
1039
|
+
"reflect": false,
|
|
1040
|
+
"attribute": "locale"
|
|
753
1041
|
}
|
|
754
1042
|
};
|
|
755
1043
|
}
|
|
@@ -759,7 +1047,8 @@ export class AuthheroNode {
|
|
|
759
1047
|
"selectedCountry": {},
|
|
760
1048
|
"localPhoneNumber": {},
|
|
761
1049
|
"countryDropdownOpen": {},
|
|
762
|
-
"telEmailMode": {}
|
|
1050
|
+
"telEmailMode": {},
|
|
1051
|
+
"dateSegments": {}
|
|
763
1052
|
};
|
|
764
1053
|
}
|
|
765
1054
|
static get events() {
|