@antadesign/anta 0.3.3 → 0.3.5
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/anta_helpers.d.ts +0 -4
- package/dist/anta_helpers.js +0 -7
- package/dist/components/Calendar.d.ts +9 -3
- package/dist/components/Calendar.js +24 -7
- package/dist/components/InputDate.d.ts +9 -6
- package/dist/components/InputDate.js +83 -143
- package/dist/components/InputDate.module.css +1 -1
- package/dist/components/InputTime.d.ts +105 -0
- package/dist/components/InputTime.js +110 -0
- package/dist/components/Menu.d.ts +15 -1
- package/dist/components/Menu.js +2 -0
- package/dist/components/MenuItem.js +1 -7
- package/dist/components/Select.d.ts +96 -14
- package/dist/components/Select.js +69 -8
- package/dist/components/Select.module.css +1 -1
- package/dist/components/SelectFaceted.d.ts +184 -0
- package/dist/components/SelectFaceted.js +331 -0
- package/dist/components/SelectFaceted.module.css +1 -0
- package/dist/components/TabPanel.d.ts +21 -13
- package/dist/components/TabPanel.js +13 -2
- package/dist/components/Tabs.d.ts +76 -52
- package/dist/components/Tabs.js +18 -86
- package/dist/elements/a-button.css +1 -1
- package/dist/elements/a-calendar.css +1 -1
- package/dist/elements/a-checkbox.css +1 -1
- package/dist/elements/a-input-time.css +1 -0
- package/dist/elements/a-input-time.d.ts +27 -0
- package/dist/elements/a-input-time.js +856 -0
- package/dist/elements/a-input.css +1 -1
- package/dist/elements/a-input.js +8 -1
- package/dist/elements/a-menu-item.css +1 -1
- package/dist/elements/a-menu-item.d.ts +8 -6
- package/dist/elements/a-menu.d.ts +13 -15
- package/dist/elements/a-menu.js +71 -25
- package/dist/elements/a-radio.css +1 -1
- package/dist/elements/a-tab.css +1 -1
- package/dist/elements/a-tabpanel.css +1 -1
- package/dist/elements/a-tabpanel.d.ts +17 -0
- package/dist/elements/a-tabpanel.js +66 -0
- package/dist/elements/a-tabs.css +1 -1
- package/dist/elements/a-tooltip.js +1 -1
- package/dist/elements/index.d.ts +2 -1
- package/dist/elements/index.js +6 -1
- package/dist/general_types.d.ts +83 -11
- package/dist/index.d.ts +7 -5
- package/dist/index.js +6 -3
- package/dist/jsx-runtime.d.ts +2 -1
- package/dist/reset.css +1 -1
- package/dist/tokens.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
import { HTMLElementBase } from "../anta_helpers";
|
|
2
|
+
import "./a-input-time.css";
|
|
3
|
+
const FORWARDED = ["name", "aria-label", "required"];
|
|
4
|
+
const CLEAR_TRIGGER = "clearrequest";
|
|
5
|
+
const CLEAR_INPUT_EVENT = "clearinput";
|
|
6
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
7
|
+
const to24 = (h12, period) => period === "pm" ? h12 % 12 + 12 : h12 % 12;
|
|
8
|
+
const SHADOW_STYLE = `
|
|
9
|
+
:host {
|
|
10
|
+
--_fs: 15px;
|
|
11
|
+
--_lh: 20px;
|
|
12
|
+
|
|
13
|
+
display: grid;
|
|
14
|
+
grid-template-columns: minmax(0, 1fr);
|
|
15
|
+
row-gap: 4px;
|
|
16
|
+
outline: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.label {
|
|
20
|
+
display: none;
|
|
21
|
+
color: var(--input-time-label);
|
|
22
|
+
font-family: var(--sans-serif);
|
|
23
|
+
font-size: var(--_fs);
|
|
24
|
+
line-height: var(--_lh);
|
|
25
|
+
font-weight: 500;
|
|
26
|
+
}
|
|
27
|
+
.label.has-label { display: block; }
|
|
28
|
+
|
|
29
|
+
.field {
|
|
30
|
+
--_bc: var(--input-time-border);
|
|
31
|
+
--_bw: 0.5px;
|
|
32
|
+
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
min-height: 28px;
|
|
37
|
+
background: var(--input-time-bg);
|
|
38
|
+
border-radius: 4px;
|
|
39
|
+
box-shadow: inset 0 0 0 var(--_bw) var(--_bc);
|
|
40
|
+
transition: box-shadow 120ms ease;
|
|
41
|
+
}
|
|
42
|
+
:host([status]) .field { --_bw: 1px; }
|
|
43
|
+
:host([size="small"]) { --_fs: 13px; --_lh: 16px; }
|
|
44
|
+
:host([size="large"]) { --_fs: 17px; --_lh: 22px; }
|
|
45
|
+
:host([size="small"]) .field { min-height: 24px; }
|
|
46
|
+
:host([size="large"]) .field { min-height: 32px; }
|
|
47
|
+
:host([round]) .field { border-radius: var(--input-time-round, 999px); }
|
|
48
|
+
|
|
49
|
+
@media (hover: hover) and (pointer: fine) {
|
|
50
|
+
:host(:not(:disabled)) .field:hover { --_bc: var(--input-time-border-hover); }
|
|
51
|
+
}
|
|
52
|
+
.field:has(.seg:focus) {
|
|
53
|
+
--_bc: var(--input-time-border-hover);
|
|
54
|
+
outline: 1px solid var(--focus-ring);
|
|
55
|
+
outline-offset: 1px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
slot[name="leading"] {
|
|
59
|
+
display: none;
|
|
60
|
+
color: var(--input-time-adornment);
|
|
61
|
+
font-size: var(--_fs);
|
|
62
|
+
line-height: var(--_lh);
|
|
63
|
+
}
|
|
64
|
+
.field.has-leading slot[name="leading"] {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
flex-shrink: 0;
|
|
68
|
+
margin-inline-start: 7px;
|
|
69
|
+
}
|
|
70
|
+
.field.has-leading .segments { padding-inline-start: 5px; }
|
|
71
|
+
:host([dim-actions]) slot[name="leading"] { opacity: 0.6; transition: opacity 120ms ease; }
|
|
72
|
+
:host([dim-actions]:not(:disabled)) .field:hover slot[name="leading"],
|
|
73
|
+
:host([dim-actions]:not(:disabled)) .field:focus-within slot[name="leading"] { opacity: 1; }
|
|
74
|
+
:host(:disabled) slot[name="leading"] { opacity: 0.5; pointer-events: none; }
|
|
75
|
+
|
|
76
|
+
.segments {
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
flex: 1 1 auto;
|
|
80
|
+
min-width: 0;
|
|
81
|
+
padding-inline: 7px;
|
|
82
|
+
color: var(--input-time-text);
|
|
83
|
+
font-family: var(--sans-serif);
|
|
84
|
+
font-feature-settings: 'ss02', 'ss05', 'tnum';
|
|
85
|
+
font-variation-settings: 'wdth' 100, 'slnt' 0, 'ital' 0;
|
|
86
|
+
font-size: var(--_fs);
|
|
87
|
+
line-height: var(--_lh);
|
|
88
|
+
font-weight: 400;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.seg {
|
|
92
|
+
display: inline-block;
|
|
93
|
+
box-sizing: border-box;
|
|
94
|
+
padding-inline: 1px;
|
|
95
|
+
border-radius: 2px;
|
|
96
|
+
text-align: center;
|
|
97
|
+
outline: none;
|
|
98
|
+
cursor: default;
|
|
99
|
+
font-variant-numeric: tabular-nums;
|
|
100
|
+
caret-color: transparent;
|
|
101
|
+
}
|
|
102
|
+
.seg[inputmode="numeric"] {
|
|
103
|
+
/* Fixed to two tabular digits so the box never reflows: neither the narrower
|
|
104
|
+
placeholder dashes nor a mid-entry single digit changes the field width. */
|
|
105
|
+
width: calc(2ch + 2px);
|
|
106
|
+
}
|
|
107
|
+
.seg:focus {
|
|
108
|
+
background: var(--input-time-seg-focus-bg);
|
|
109
|
+
color: var(--input-time-seg-focus-text);
|
|
110
|
+
}
|
|
111
|
+
.seg[data-placeholder] { color: var(--input-time-placeholder); }
|
|
112
|
+
.lit { white-space: pre; color: var(--input-time-text); }
|
|
113
|
+
|
|
114
|
+
/* No text-selection highlight inside the field: the segments are spinbuttons,
|
|
115
|
+
and a selection painted over the focused segment's tint compounds into an
|
|
116
|
+
unreadable block. The focus tint alone marks the active segment. Scoped to
|
|
117
|
+
the shadow, so page selection is unaffected. */
|
|
118
|
+
::selection { background: transparent; }
|
|
119
|
+
|
|
120
|
+
:host(:disabled) .segments { cursor: not-allowed; }
|
|
121
|
+
:host(:disabled) .seg { cursor: not-allowed; }
|
|
122
|
+
|
|
123
|
+
slot[name="trailing"] { display: none; }
|
|
124
|
+
.field.has-trailing slot[name="trailing"] {
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
gap: 2px;
|
|
128
|
+
flex-shrink: 0;
|
|
129
|
+
color: var(--input-time-adornment);
|
|
130
|
+
font-size: var(--_fs);
|
|
131
|
+
}
|
|
132
|
+
:host([dim-actions]) slot[name="trailing"],
|
|
133
|
+
:host([dim-actions]) slot[name="clear"] { opacity: 0.6; transition: opacity 120ms ease; }
|
|
134
|
+
:host([dim-actions]:not(:disabled)) .field:hover slot[name="trailing"],
|
|
135
|
+
:host([dim-actions]:not(:disabled)) .field:hover slot[name="clear"],
|
|
136
|
+
:host([dim-actions]:not(:disabled)) .field:focus-within slot[name="trailing"],
|
|
137
|
+
:host([dim-actions]:not(:disabled)) .field:focus-within slot[name="clear"] { opacity: 1; }
|
|
138
|
+
|
|
139
|
+
slot[name="clear"] {
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
flex-shrink: 0;
|
|
143
|
+
visibility: hidden;
|
|
144
|
+
}
|
|
145
|
+
.field.is-filled slot[name="clear"] { visibility: visible; }
|
|
146
|
+
:host(:disabled) slot[name="clear"] { display: none; }
|
|
147
|
+
|
|
148
|
+
.hint {
|
|
149
|
+
display: none;
|
|
150
|
+
gap: 4px;
|
|
151
|
+
align-items: flex-start;
|
|
152
|
+
padding-inline-start: 1px;
|
|
153
|
+
color: var(--input-time-hint);
|
|
154
|
+
font-family: var(--sans-serif);
|
|
155
|
+
font-size: calc(var(--_fs) - 1px);
|
|
156
|
+
line-height: calc(var(--_lh) - 2px);
|
|
157
|
+
}
|
|
158
|
+
.hint.has-hint { display: flex; }
|
|
159
|
+
`;
|
|
160
|
+
class AInputTimeElement extends HTMLElementBase {
|
|
161
|
+
static formAssociated = true;
|
|
162
|
+
static observedAttributes = [
|
|
163
|
+
...FORWARDED,
|
|
164
|
+
"value",
|
|
165
|
+
"defaultvalue",
|
|
166
|
+
"locale",
|
|
167
|
+
"hour12",
|
|
168
|
+
"status",
|
|
169
|
+
"disabled",
|
|
170
|
+
"size",
|
|
171
|
+
"min",
|
|
172
|
+
"max"
|
|
173
|
+
];
|
|
174
|
+
#internals;
|
|
175
|
+
#field;
|
|
176
|
+
#labelBox;
|
|
177
|
+
#labelSlot;
|
|
178
|
+
#hintBox;
|
|
179
|
+
#hintSlot;
|
|
180
|
+
#leadingSlot;
|
|
181
|
+
#trailingSlot;
|
|
182
|
+
#segRow;
|
|
183
|
+
#ready = false;
|
|
184
|
+
#formDisabled = false;
|
|
185
|
+
// True once a value arrived via the `value` property setter — so connect
|
|
186
|
+
// doesn't re-seed from the (possibly empty) attribute and wipe it.
|
|
187
|
+
#seeded = false;
|
|
188
|
+
// The canonical 24-hour "HH:mm" (or ''), kept in step with the segment state.
|
|
189
|
+
// Mode-independent, so a locale / hour12 switch re-derives display from it
|
|
190
|
+
// rather than from the now-stale display digits.
|
|
191
|
+
#iso = "";
|
|
192
|
+
// Segment state — display units: hour is 0–23 (24h) or 1–12 (12h); period is
|
|
193
|
+
// independent so it can be set/shown before the hour is. Minute is 0–59.
|
|
194
|
+
#hour = null;
|
|
195
|
+
#minute = null;
|
|
196
|
+
#period = null;
|
|
197
|
+
#segs = [];
|
|
198
|
+
// In-progress typed digits for the focused numeric segment (reset on blur /
|
|
199
|
+
// segment change / commit), so `1` then `2` builds `12`.
|
|
200
|
+
#buf = "";
|
|
201
|
+
constructor() {
|
|
202
|
+
super();
|
|
203
|
+
try {
|
|
204
|
+
this.#internals = this.attachInternals?.();
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.warn("a-input-time: ElementInternals unavailable \u2014 form association disabled.", err);
|
|
207
|
+
}
|
|
208
|
+
const shadow = this.attachShadow({ mode: "open", delegatesFocus: true });
|
|
209
|
+
const style = document.createElement("style");
|
|
210
|
+
style.textContent = SHADOW_STYLE;
|
|
211
|
+
this.#labelBox = document.createElement("div");
|
|
212
|
+
this.#labelBox.className = "label";
|
|
213
|
+
this.#labelBox.setAttribute("part", "label");
|
|
214
|
+
this.#labelSlot = document.createElement("slot");
|
|
215
|
+
this.#labelSlot.name = "label";
|
|
216
|
+
this.#labelBox.append(this.#labelSlot);
|
|
217
|
+
this.#labelSlot.addEventListener("click", () => this.#segs[0]?.el.focus());
|
|
218
|
+
this.#labelSlot.addEventListener("slotchange", this.#onLabelSlotChange);
|
|
219
|
+
this.#field = document.createElement("div");
|
|
220
|
+
this.#field.className = "field";
|
|
221
|
+
this.#field.setAttribute("part", "field");
|
|
222
|
+
this.#segRow = document.createElement("div");
|
|
223
|
+
this.#segRow.className = "segments";
|
|
224
|
+
this.#segRow.setAttribute("part", "segments");
|
|
225
|
+
this.#segRow.setAttribute("role", "group");
|
|
226
|
+
this.#field.addEventListener("mousedown", (e) => {
|
|
227
|
+
if (e.target === this.#field || e.target === this.#segRow) {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
this.#segs[0]?.el.focus();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
this.#leadingSlot = document.createElement("slot");
|
|
233
|
+
this.#leadingSlot.name = "leading";
|
|
234
|
+
this.#leadingSlot.setAttribute("part", "leading");
|
|
235
|
+
this.#leadingSlot.addEventListener("slotchange", () => this.#field.classList.toggle("has-leading", this.#leadingSlot.assignedNodes().length > 0));
|
|
236
|
+
const clearSlot = document.createElement("slot");
|
|
237
|
+
clearSlot.name = "clear";
|
|
238
|
+
clearSlot.setAttribute("part", "clear");
|
|
239
|
+
this.#trailingSlot = document.createElement("slot");
|
|
240
|
+
this.#trailingSlot.name = "trailing";
|
|
241
|
+
this.#trailingSlot.setAttribute("part", "trailing");
|
|
242
|
+
this.#trailingSlot.addEventListener("slotchange", () => this.#field.classList.toggle("has-trailing", this.#trailingSlot.assignedNodes().length > 0));
|
|
243
|
+
this.#field.append(this.#leadingSlot, this.#segRow, clearSlot, this.#trailingSlot);
|
|
244
|
+
this.addEventListener(CLEAR_TRIGGER, () => this.clear());
|
|
245
|
+
this.#hintBox = document.createElement("div");
|
|
246
|
+
this.#hintBox.className = "hint";
|
|
247
|
+
this.#hintBox.setAttribute("part", "hint");
|
|
248
|
+
this.#hintSlot = document.createElement("slot");
|
|
249
|
+
this.#hintSlot.name = "hint";
|
|
250
|
+
this.#hintSlot.addEventListener("slotchange", this.#onHintSlotChange);
|
|
251
|
+
this.#hintBox.append(this.#hintSlot);
|
|
252
|
+
const extrasSlot = document.createElement("slot");
|
|
253
|
+
shadow.append(style, this.#labelBox, this.#field, extrasSlot, this.#hintBox);
|
|
254
|
+
}
|
|
255
|
+
getAnchorRect() {
|
|
256
|
+
return this.#field.getBoundingClientRect();
|
|
257
|
+
}
|
|
258
|
+
connectedCallback() {
|
|
259
|
+
if (Object.prototype.hasOwnProperty.call(this, "value")) {
|
|
260
|
+
const v = this.value;
|
|
261
|
+
delete this.value;
|
|
262
|
+
this.#seed(v);
|
|
263
|
+
} else if (!this.#seeded) {
|
|
264
|
+
this.#seed(this.getAttribute("value") ?? this.getAttribute("defaultvalue") ?? "");
|
|
265
|
+
}
|
|
266
|
+
this.#buildSegments();
|
|
267
|
+
this.#ready = true;
|
|
268
|
+
}
|
|
269
|
+
/** Seed the value state + canonical cache before the segments exist. */
|
|
270
|
+
#seed(v) {
|
|
271
|
+
this.#applyValue(v);
|
|
272
|
+
this.#iso = this.value;
|
|
273
|
+
}
|
|
274
|
+
attributeChangedCallback(name, _old, value) {
|
|
275
|
+
if (!this.#ready) return;
|
|
276
|
+
if (name === "value") {
|
|
277
|
+
if (value !== null) this.value = value;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (name === "defaultvalue") return;
|
|
281
|
+
if (name === "locale" || name === "hour12") {
|
|
282
|
+
this.#buildSegments();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (name === "status") {
|
|
286
|
+
this.#syncStatus();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (name === "disabled") {
|
|
290
|
+
this.#syncDisabled();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (name === "size") return;
|
|
294
|
+
if (name === "min" || name === "max") {
|
|
295
|
+
this.#clampIfComplete();
|
|
296
|
+
this.#iso = this.value;
|
|
297
|
+
this.#render();
|
|
298
|
+
this.#internals?.setFormValue(this.value);
|
|
299
|
+
this.#updateFilled();
|
|
300
|
+
this.#updateValidity();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (name === "aria-label") {
|
|
304
|
+
this.#applyGroupLabel();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (name === "required") {
|
|
308
|
+
this.#updateValidity();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// --- Locale-driven segment construction ---
|
|
313
|
+
get #locale() {
|
|
314
|
+
return this.getAttribute("locale") || (typeof navigator !== "undefined" ? navigator.language : "en-US");
|
|
315
|
+
}
|
|
316
|
+
get #hour12() {
|
|
317
|
+
const attr = this.getAttribute("hour12");
|
|
318
|
+
if (attr === "true") return true;
|
|
319
|
+
if (attr === "false") return false;
|
|
320
|
+
try {
|
|
321
|
+
const hc = new Intl.DateTimeFormat(this.#locale, { hour: "numeric" }).resolvedOptions().hourCycle;
|
|
322
|
+
return hc === "h11" || hc === "h12";
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/** Rebuild the segment row from the locale: order, separators, and whether an
|
|
328
|
+
* AM/PM segment exists all come from `Intl.formatToParts`. Preserves the
|
|
329
|
+
* current value across a rebuild (e.g. locale/hour12 change). */
|
|
330
|
+
#buildSegments() {
|
|
331
|
+
const hour12 = this.#hour12;
|
|
332
|
+
const locale = this.#locale;
|
|
333
|
+
const keep = this.#iso;
|
|
334
|
+
let parts = [];
|
|
335
|
+
let amText = "AM";
|
|
336
|
+
let pmText = "PM";
|
|
337
|
+
try {
|
|
338
|
+
const fmt = new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "numeric", hour12 });
|
|
339
|
+
parts = fmt.formatToParts(new Date(2e3, 0, 1, 13, 5));
|
|
340
|
+
const am = fmt.formatToParts(new Date(2e3, 0, 1, 1, 5)).find((p) => p.type === "dayPeriod");
|
|
341
|
+
const pm = parts.find((p) => p.type === "dayPeriod");
|
|
342
|
+
if (am?.value) amText = am.value;
|
|
343
|
+
if (pm?.value) pmText = pm.value;
|
|
344
|
+
} catch {
|
|
345
|
+
parts = [
|
|
346
|
+
{ type: "hour", value: "13" },
|
|
347
|
+
{ type: "literal", value: ":" },
|
|
348
|
+
{ type: "minute", value: "05" }
|
|
349
|
+
];
|
|
350
|
+
}
|
|
351
|
+
this.#segRow.replaceChildren();
|
|
352
|
+
this.#segs = [];
|
|
353
|
+
for (const part of parts) {
|
|
354
|
+
if (part.type === "hour" || part.type === "minute" || part.type === "dayPeriod") {
|
|
355
|
+
const el = document.createElement("span");
|
|
356
|
+
el.className = "seg";
|
|
357
|
+
el.tabIndex = 0;
|
|
358
|
+
el.setAttribute("role", "spinbutton");
|
|
359
|
+
el.contentEditable = "true";
|
|
360
|
+
el.spellcheck = false;
|
|
361
|
+
el.setAttribute("autocorrect", "off");
|
|
362
|
+
el.setAttribute("autocapitalize", "off");
|
|
363
|
+
const kind = part.type === "dayPeriod" ? "period" : part.type;
|
|
364
|
+
el.setAttribute("aria-label", kind === "hour" ? "Hour" : kind === "minute" ? "Minute" : "AM/PM");
|
|
365
|
+
el.setAttribute("inputmode", kind === "period" ? "text" : "numeric");
|
|
366
|
+
const seg = kind === "hour" ? { kind, el, min: hour12 ? 1 : 0, max: hour12 ? 12 : 23 } : kind === "minute" ? { kind, el, min: 0, max: 59 } : { kind, el, min: 0, max: 1 };
|
|
367
|
+
el.addEventListener("keydown", (e) => this.#onSegKeyDown(e, seg));
|
|
368
|
+
el.addEventListener("beforeinput", (e) => this.#onSegBeforeInput(e, seg));
|
|
369
|
+
el.addEventListener("focus", () => {
|
|
370
|
+
this.#buf = "";
|
|
371
|
+
});
|
|
372
|
+
el.addEventListener("blur", () => this.#onSegBlur());
|
|
373
|
+
this.#segRow.append(el);
|
|
374
|
+
this.#segs.push(seg);
|
|
375
|
+
} else {
|
|
376
|
+
const lit = document.createElement("span");
|
|
377
|
+
lit.className = "lit";
|
|
378
|
+
lit.setAttribute("aria-hidden", "true");
|
|
379
|
+
lit.textContent = part.value;
|
|
380
|
+
this.#segRow.append(lit);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
this.#amText = amText;
|
|
384
|
+
this.#pmText = pmText;
|
|
385
|
+
this.#applyValue(keep);
|
|
386
|
+
this.#applyGroupLabel();
|
|
387
|
+
this.#syncStatus();
|
|
388
|
+
this.#syncDisabled();
|
|
389
|
+
this.#render();
|
|
390
|
+
this.#internals?.setFormValue(this.value);
|
|
391
|
+
this.#updateFilled();
|
|
392
|
+
this.#updateValidity();
|
|
393
|
+
}
|
|
394
|
+
#amText = "AM";
|
|
395
|
+
#pmText = "PM";
|
|
396
|
+
// --- Keyboard ---
|
|
397
|
+
// Navigation + stepping only. Character entry and deletion go through
|
|
398
|
+
// `#onSegBeforeInput` (so mobile virtual keyboards, which don't fire keydown
|
|
399
|
+
// reliably, work) — these keys don't produce `beforeinput`, so there's no
|
|
400
|
+
// double handling.
|
|
401
|
+
#onSegKeyDown(e, seg) {
|
|
402
|
+
if (this.hasAttribute("disabled") || this.#formDisabled) return;
|
|
403
|
+
const k = e.key;
|
|
404
|
+
if (k === "ArrowUp" || k === "ArrowDown") {
|
|
405
|
+
e.preventDefault();
|
|
406
|
+
this.#step(seg, k === "ArrowUp" ? 1 : -1);
|
|
407
|
+
} else if (k === "PageUp" || k === "PageDown") {
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
this.#step(seg, (k === "PageUp" ? 1 : -1) * (seg.kind === "minute" ? 5 : 1));
|
|
410
|
+
} else if (k === "Home") {
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
this.#set(seg, seg.min);
|
|
413
|
+
} else if (k === "End") {
|
|
414
|
+
e.preventDefault();
|
|
415
|
+
this.#set(seg, seg.max);
|
|
416
|
+
} else if (k === "ArrowLeft" || k === "ArrowRight") {
|
|
417
|
+
e.preventDefault();
|
|
418
|
+
this.#moveFocus(seg, k === "ArrowRight" ? 1 : -1);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Character entry (digits, a/p) and deletion, from a physical or virtual
|
|
422
|
+
// keyboard. We never let the contentEditable mutate — preventDefault every
|
|
423
|
+
// `beforeinput` and route it, keeping the segment text under our control.
|
|
424
|
+
#onSegBeforeInput(e, seg) {
|
|
425
|
+
e.preventDefault();
|
|
426
|
+
if (this.hasAttribute("disabled") || this.#formDisabled) return;
|
|
427
|
+
const type = e.inputType;
|
|
428
|
+
if (type === "deleteContentBackward" || type === "deleteContentForward") {
|
|
429
|
+
this.#buf = "";
|
|
430
|
+
this.#clearSeg(seg);
|
|
431
|
+
if (type === "deleteContentBackward") this.#moveFocus(seg, -1);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (type === "insertFromPaste") {
|
|
435
|
+
const text = e.dataTransfer?.getData("text/plain") || e.data || "";
|
|
436
|
+
const parsed = this.#parsePaste(text);
|
|
437
|
+
if (parsed) {
|
|
438
|
+
this.#buf = "";
|
|
439
|
+
this.#applyValue(`${pad2(parsed.h)}:${pad2(parsed.min)}`);
|
|
440
|
+
this.#clampIfComplete();
|
|
441
|
+
this.#commitEdit();
|
|
442
|
+
this.#dispatch("change");
|
|
443
|
+
} else {
|
|
444
|
+
for (const ch of text) this.#typeChar(seg, ch);
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (type.startsWith("insert")) {
|
|
449
|
+
for (const ch of e.data ?? "") this.#typeChar(seg, ch);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/** Parse a pasted time to 24-hour `{ h, min }`, or null. Accepts `14:30`,
|
|
453
|
+
* `9:5`, `2:30 pm`, `12am`, and run-together `230` / `1430` — mirrors
|
|
454
|
+
* `calendar-core`'s `parseTimeInput` but Temporal-free, so the element's
|
|
455
|
+
* granular import stays lean. */
|
|
456
|
+
#parsePaste(text) {
|
|
457
|
+
let s = (text ?? "").trim().toLowerCase();
|
|
458
|
+
if (!s) return null;
|
|
459
|
+
let mer = null;
|
|
460
|
+
const m = s.match(/([ap])\.?m?\.?$/);
|
|
461
|
+
if (m) {
|
|
462
|
+
mer = m[1] === "p" ? "pm" : "am";
|
|
463
|
+
s = s.slice(0, m.index).trim();
|
|
464
|
+
}
|
|
465
|
+
let h;
|
|
466
|
+
let min;
|
|
467
|
+
if (/[:.]/.test(s)) {
|
|
468
|
+
const [hp, mp] = s.split(/[:.]/);
|
|
469
|
+
h = Number(hp);
|
|
470
|
+
min = Number(mp ?? "0");
|
|
471
|
+
} else {
|
|
472
|
+
const d = s.replace(/\D/g, "");
|
|
473
|
+
if (!d) return null;
|
|
474
|
+
if (d.length <= 2) {
|
|
475
|
+
h = Number(d);
|
|
476
|
+
min = 0;
|
|
477
|
+
} else {
|
|
478
|
+
const cut = d.length - 2;
|
|
479
|
+
h = Number(d.slice(0, cut));
|
|
480
|
+
min = Number(d.slice(cut));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (!Number.isInteger(h) || !Number.isInteger(min)) return null;
|
|
484
|
+
if (mer === "pm" && h < 12) h += 12;
|
|
485
|
+
if (mer === "am" && h === 12) h = 0;
|
|
486
|
+
if (h < 0 || h > 23 || min < 0 || min > 59) return null;
|
|
487
|
+
return { h, min };
|
|
488
|
+
}
|
|
489
|
+
#typeChar(seg, ch) {
|
|
490
|
+
if (seg.kind === "period") {
|
|
491
|
+
const c = ch.toLowerCase();
|
|
492
|
+
if (c === "a" || c === "p") {
|
|
493
|
+
this.#period = c === "p" ? "pm" : "am";
|
|
494
|
+
this.#commitEdit();
|
|
495
|
+
this.#moveFocus(seg, 1);
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (/[0-9]/.test(ch)) this.#typeDigit(seg, ch);
|
|
500
|
+
}
|
|
501
|
+
/** Arrow / page / home / end increment: wraps within a segment's [min, max];
|
|
502
|
+
* from empty, ↑ lands on min and ↓ on max. Clamps the resulting complete value
|
|
503
|
+
* into the field's `min`/`max` range (so ↑ can't step past `max`). */
|
|
504
|
+
#step(seg, delta) {
|
|
505
|
+
this.#buf = "";
|
|
506
|
+
if (seg.kind === "period") {
|
|
507
|
+
this.#period = this.#period == null ? "am" : this.#period === "am" ? "pm" : "am";
|
|
508
|
+
this.#clampIfComplete();
|
|
509
|
+
this.#commitEdit();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const cur = seg.kind === "hour" ? this.#hour : this.#minute;
|
|
513
|
+
const span = seg.max - seg.min + 1;
|
|
514
|
+
let next;
|
|
515
|
+
if (cur == null) next = delta > 0 ? seg.min : seg.max;
|
|
516
|
+
else next = ((cur - seg.min + delta) % span + span) % span + seg.min;
|
|
517
|
+
this.#assign(seg, next);
|
|
518
|
+
this.#clampIfComplete();
|
|
519
|
+
this.#commitEdit();
|
|
520
|
+
}
|
|
521
|
+
/** Mutate a segment's state without committing (so callers can clamp first). */
|
|
522
|
+
#assign(seg, val) {
|
|
523
|
+
if (seg.kind === "period") return;
|
|
524
|
+
if (seg.kind === "hour") this.#hour = val;
|
|
525
|
+
else this.#minute = val;
|
|
526
|
+
if (seg.kind === "hour" && this.#hour12 && this.#period == null) this.#period = "am";
|
|
527
|
+
}
|
|
528
|
+
#set(seg, val) {
|
|
529
|
+
this.#assign(seg, val);
|
|
530
|
+
this.#commitEdit();
|
|
531
|
+
}
|
|
532
|
+
// --- min / max range (total minutes since 00:00, or null when unbounded) ---
|
|
533
|
+
#parseTotal(v) {
|
|
534
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec((v ?? "").trim());
|
|
535
|
+
if (!m) return null;
|
|
536
|
+
return Math.min(23, Number(m[1])) * 60 + Math.min(59, Number(m[2]));
|
|
537
|
+
}
|
|
538
|
+
get #minTotal() {
|
|
539
|
+
return this.#parseTotal(this.getAttribute("min"));
|
|
540
|
+
}
|
|
541
|
+
get #maxTotal() {
|
|
542
|
+
return this.#parseTotal(this.getAttribute("max"));
|
|
543
|
+
}
|
|
544
|
+
#currentTotal() {
|
|
545
|
+
const h = this.#h24;
|
|
546
|
+
if (h == null || this.#minute == null) return null;
|
|
547
|
+
return h * 60 + this.#minute;
|
|
548
|
+
}
|
|
549
|
+
/** If the value is complete and outside `min`/`max`, snap it to the nearest
|
|
550
|
+
* bound and re-derive the segments. Returns whether it changed. */
|
|
551
|
+
#clampIfComplete() {
|
|
552
|
+
const total = this.#currentTotal();
|
|
553
|
+
if (total == null) return false;
|
|
554
|
+
const lo = this.#minTotal;
|
|
555
|
+
const hi = this.#maxTotal;
|
|
556
|
+
let clamped = total;
|
|
557
|
+
if (lo != null && clamped < lo) clamped = lo;
|
|
558
|
+
if (hi != null && clamped > hi) clamped = hi;
|
|
559
|
+
if (clamped === total) return false;
|
|
560
|
+
this.#applyValue(`${pad2(Math.floor(clamped / 60))}:${pad2(clamped % 60)}`);
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
#clearSeg(seg) {
|
|
564
|
+
if (seg.kind === "hour") this.#hour = null;
|
|
565
|
+
else if (seg.kind === "minute") this.#minute = null;
|
|
566
|
+
else this.#period = null;
|
|
567
|
+
this.#commitEdit();
|
|
568
|
+
}
|
|
569
|
+
/** Type a digit into a numeric segment, accumulating and auto-advancing once the
|
|
570
|
+
* segment can't take another digit (the `Number(v + '0') > max` heuristic). */
|
|
571
|
+
#typeDigit(seg, d) {
|
|
572
|
+
const hour12Hour = seg.kind === "hour" && this.#hour12;
|
|
573
|
+
const typeMax = hour12Hour ? 23 : seg.max;
|
|
574
|
+
const candidate = Number(this.#buf + d);
|
|
575
|
+
let raw;
|
|
576
|
+
let advance = false;
|
|
577
|
+
if (candidate > typeMax) {
|
|
578
|
+
raw = Number(d);
|
|
579
|
+
this.#buf = d;
|
|
580
|
+
} else {
|
|
581
|
+
raw = candidate;
|
|
582
|
+
this.#buf += d;
|
|
583
|
+
}
|
|
584
|
+
if (Number(`${raw}0`) > typeMax || this.#buf.length >= String(typeMax).length) {
|
|
585
|
+
advance = true;
|
|
586
|
+
this.#buf = "";
|
|
587
|
+
}
|
|
588
|
+
if (hour12Hour) {
|
|
589
|
+
if (raw === 0 && !advance) {
|
|
590
|
+
seg.el.textContent = "0";
|
|
591
|
+
seg.el.setAttribute("data-placeholder", "");
|
|
592
|
+
seg.el.removeAttribute("aria-valuenow");
|
|
593
|
+
seg.el.removeAttribute("aria-valuetext");
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (raw === 0) {
|
|
597
|
+
this.#hour = 12;
|
|
598
|
+
this.#period = "am";
|
|
599
|
+
} else if (raw > 12) {
|
|
600
|
+
this.#hour = raw - 12;
|
|
601
|
+
this.#period = "pm";
|
|
602
|
+
} else {
|
|
603
|
+
this.#hour = raw;
|
|
604
|
+
if (this.#period == null) this.#period = "am";
|
|
605
|
+
}
|
|
606
|
+
this.#commitEdit();
|
|
607
|
+
if (advance) this.#moveFocus(seg, 1);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
this.#assign(seg, Math.max(seg.min, raw));
|
|
611
|
+
this.#commitEdit();
|
|
612
|
+
if (advance) this.#moveFocus(seg, 1);
|
|
613
|
+
}
|
|
614
|
+
#moveFocus(from, dir) {
|
|
615
|
+
const i = this.#segs.indexOf(from);
|
|
616
|
+
const next = this.#segs[i + dir];
|
|
617
|
+
if (next) next.el.focus();
|
|
618
|
+
}
|
|
619
|
+
/** On losing a segment, reset the digit buffer; when focus has left the whole
|
|
620
|
+
* group (not just hopped to a sibling segment), clamp into `min`/`max` and
|
|
621
|
+
* fire `change`. `shadowRoot.activeElement` reliably reports the focused
|
|
622
|
+
* segment even across the shadow boundary (blur `relatedTarget` is retargeted
|
|
623
|
+
* to null), so defer one microtask to read where focus landed. */
|
|
624
|
+
#onSegBlur() {
|
|
625
|
+
this.#buf = "";
|
|
626
|
+
queueMicrotask(() => {
|
|
627
|
+
const active = this.shadowRoot?.activeElement;
|
|
628
|
+
if (this.#segs.some((s) => s.el === active)) return;
|
|
629
|
+
if (this.#clampIfComplete()) this.#commitEdit();
|
|
630
|
+
this.#dispatch("change");
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
// --- Value + rendering ---
|
|
634
|
+
/** Canonical 0–23 hour from the display state, or null when incomplete. */
|
|
635
|
+
get #h24() {
|
|
636
|
+
if (this.#hour == null) return null;
|
|
637
|
+
if (!this.#hour12) return this.#hour;
|
|
638
|
+
if (this.#period == null) return null;
|
|
639
|
+
return to24(this.#hour, this.#period);
|
|
640
|
+
}
|
|
641
|
+
#commitEdit() {
|
|
642
|
+
this.#render();
|
|
643
|
+
this.#iso = this.value;
|
|
644
|
+
this.#internals?.setFormValue(this.value);
|
|
645
|
+
this.#updateFilled();
|
|
646
|
+
this.#updateValidity();
|
|
647
|
+
this.#dispatch("input");
|
|
648
|
+
}
|
|
649
|
+
#render() {
|
|
650
|
+
for (const seg of this.#segs) {
|
|
651
|
+
if (seg.kind === "period") this.#renderSeg(seg, null);
|
|
652
|
+
else this.#renderSeg(seg, seg.kind === "hour" ? this.#hour : this.#minute);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/** Paint one segment: its value (or placeholder) as text, plus the spinbutton
|
|
656
|
+
* ARIA values. `override` lets `#typeDigit` show a partial value mid-entry. */
|
|
657
|
+
#renderSeg(seg, override) {
|
|
658
|
+
const el = seg.el;
|
|
659
|
+
if (seg.kind === "period") {
|
|
660
|
+
const set2 = this.#period != null;
|
|
661
|
+
el.textContent = set2 ? this.#period === "pm" ? this.#pmText : this.#amText : this.#amText;
|
|
662
|
+
el.toggleAttribute("data-placeholder", !set2);
|
|
663
|
+
el.setAttribute("aria-valuemin", "0");
|
|
664
|
+
el.setAttribute("aria-valuemax", "1");
|
|
665
|
+
if (set2) {
|
|
666
|
+
el.setAttribute("aria-valuenow", this.#period === "pm" ? "1" : "0");
|
|
667
|
+
el.setAttribute("aria-valuetext", this.#period === "pm" ? this.#pmText : this.#amText);
|
|
668
|
+
} else {
|
|
669
|
+
el.removeAttribute("aria-valuenow");
|
|
670
|
+
el.removeAttribute("aria-valuetext");
|
|
671
|
+
}
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const val = override;
|
|
675
|
+
const set = val != null;
|
|
676
|
+
el.textContent = set ? pad2(val) : "\u2013\u2013";
|
|
677
|
+
el.toggleAttribute("data-placeholder", !set);
|
|
678
|
+
el.setAttribute("aria-valuemin", String(seg.min));
|
|
679
|
+
el.setAttribute("aria-valuemax", String(seg.max));
|
|
680
|
+
if (set) {
|
|
681
|
+
el.setAttribute("aria-valuenow", String(val));
|
|
682
|
+
el.setAttribute("aria-valuetext", pad2(val));
|
|
683
|
+
} else {
|
|
684
|
+
el.removeAttribute("aria-valuenow");
|
|
685
|
+
el.removeAttribute("aria-valuetext");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
/** Parse a `"HH:mm"` value into the display segments (24h → display units). */
|
|
689
|
+
#applyValue(v) {
|
|
690
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec((v ?? "").trim());
|
|
691
|
+
if (!m) {
|
|
692
|
+
this.#hour = null;
|
|
693
|
+
this.#minute = null;
|
|
694
|
+
this.#period = null;
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const h = Math.min(23, Number(m[1]));
|
|
698
|
+
const min = Math.min(59, Number(m[2]));
|
|
699
|
+
this.#minute = min;
|
|
700
|
+
if (this.#hour12) {
|
|
701
|
+
this.#hour = (h + 11) % 12 + 1;
|
|
702
|
+
this.#period = h < 12 ? "am" : "pm";
|
|
703
|
+
} else {
|
|
704
|
+
this.#hour = h;
|
|
705
|
+
this.#period = null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
#dispatch(type) {
|
|
709
|
+
this.dispatchEvent(new Event(type, { bubbles: true, composed: type === "input" }));
|
|
710
|
+
}
|
|
711
|
+
#applyGroupLabel() {
|
|
712
|
+
const label = this.getAttribute("aria-label") || this.#labelSlot.assignedNodes().map((n) => n.textContent ?? "").join(" ").trim();
|
|
713
|
+
if (label) this.#segRow.setAttribute("aria-label", label);
|
|
714
|
+
else this.#segRow.removeAttribute("aria-label");
|
|
715
|
+
}
|
|
716
|
+
#onLabelSlotChange = () => {
|
|
717
|
+
this.#labelBox.classList.toggle("has-label", this.#labelSlot.assignedNodes().length > 0);
|
|
718
|
+
this.#applyGroupLabel();
|
|
719
|
+
};
|
|
720
|
+
#onHintSlotChange = () => {
|
|
721
|
+
this.#hintBox.classList.toggle("has-hint", this.#hintSlot.assignedNodes().length > 0);
|
|
722
|
+
};
|
|
723
|
+
#syncDisabled() {
|
|
724
|
+
const off = this.hasAttribute("disabled") || this.#formDisabled;
|
|
725
|
+
for (const seg of this.#segs) {
|
|
726
|
+
if (off) seg.el.setAttribute("aria-disabled", "true");
|
|
727
|
+
else seg.el.removeAttribute("aria-disabled");
|
|
728
|
+
seg.el.tabIndex = off ? -1 : 0;
|
|
729
|
+
seg.el.contentEditable = off ? "false" : "true";
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
#syncStatus() {
|
|
733
|
+
const critical = this.getAttribute("status") === "critical";
|
|
734
|
+
for (const seg of this.#segs) seg.el.setAttribute("aria-invalid", critical ? "true" : "false");
|
|
735
|
+
try {
|
|
736
|
+
critical ? this.#internals?.states.add("invalid") : this.#internals?.states.delete("invalid");
|
|
737
|
+
} catch {
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
#updateFilled() {
|
|
741
|
+
const filled = !!this.value;
|
|
742
|
+
this.#field.classList.toggle("is-filled", filled);
|
|
743
|
+
try {
|
|
744
|
+
if (filled) this.#internals?.states.add("filled");
|
|
745
|
+
else this.#internals?.states.delete("filled");
|
|
746
|
+
} catch {
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
#updateValidity() {
|
|
750
|
+
if (!this.#internals) return;
|
|
751
|
+
const anchor = this.#segs[0]?.el ?? this;
|
|
752
|
+
const required = this.hasAttribute("required");
|
|
753
|
+
const total = this.#currentTotal();
|
|
754
|
+
const lo = this.#minTotal;
|
|
755
|
+
const hi = this.#maxTotal;
|
|
756
|
+
if (required && !this.value) {
|
|
757
|
+
this.#internals.setValidity({ valueMissing: true }, "Please enter a time.", anchor);
|
|
758
|
+
} else if (total != null && lo != null && total < lo) {
|
|
759
|
+
this.#internals.setValidity({ rangeUnderflow: true }, `Time must be ${this.getAttribute("min")} or later.`, anchor);
|
|
760
|
+
} else if (total != null && hi != null && total > hi) {
|
|
761
|
+
this.#internals.setValidity({ rangeOverflow: true }, `Time must be ${this.getAttribute("max")} or earlier.`, anchor);
|
|
762
|
+
} else if (this.getAttribute("status") === "critical") {
|
|
763
|
+
this.#internals.setValidity({ customError: true }, "Invalid value.", anchor);
|
|
764
|
+
} else {
|
|
765
|
+
this.#internals.setValidity({});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// --- Public value API (paired getter/setter, mirrors a-input) ---
|
|
769
|
+
/** The current time as 24-hour `"HH:mm"`, or `''` when incomplete. */
|
|
770
|
+
get value() {
|
|
771
|
+
const h = this.#h24;
|
|
772
|
+
if (h == null || this.#minute == null) return "";
|
|
773
|
+
return `${pad2(h)}:${pad2(this.#minute)}`;
|
|
774
|
+
}
|
|
775
|
+
set value(v) {
|
|
776
|
+
this.#seeded = true;
|
|
777
|
+
const next = v ?? "";
|
|
778
|
+
if (next === this.value) return;
|
|
779
|
+
this.#applyValue(next);
|
|
780
|
+
this.#iso = this.value;
|
|
781
|
+
if (this.#ready) {
|
|
782
|
+
this.#render();
|
|
783
|
+
this.#internals?.setFormValue(this.value);
|
|
784
|
+
this.#updateFilled();
|
|
785
|
+
this.#updateValidity();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/** Empty the field, refocus the first segment, and fire input + change. */
|
|
789
|
+
clear() {
|
|
790
|
+
this.#hour = null;
|
|
791
|
+
this.#minute = null;
|
|
792
|
+
this.#period = null;
|
|
793
|
+
this.#iso = "";
|
|
794
|
+
this.#render();
|
|
795
|
+
this.#internals?.setFormValue("");
|
|
796
|
+
this.#updateFilled();
|
|
797
|
+
this.#updateValidity();
|
|
798
|
+
this.#dispatch("input");
|
|
799
|
+
this.#dispatch("change");
|
|
800
|
+
this.dispatchEvent(new CustomEvent(CLEAR_INPUT_EVENT, { bubbles: true }));
|
|
801
|
+
this.#segs[0]?.el.focus();
|
|
802
|
+
}
|
|
803
|
+
get name() {
|
|
804
|
+
return this.getAttribute("name") ?? "";
|
|
805
|
+
}
|
|
806
|
+
set name(v) {
|
|
807
|
+
this.setAttribute("name", v);
|
|
808
|
+
}
|
|
809
|
+
// Constraint-validation API, proxied from ElementInternals (mirrors a-input) so
|
|
810
|
+
// the host behaves like a native control and the `InputTime` wrapper can read
|
|
811
|
+
// validity for its onValueChange snapshot. Read-only mirrors — allowlisted in
|
|
812
|
+
// scripts/lint-getters.mjs (no wrapper passes them as props).
|
|
813
|
+
get validity() {
|
|
814
|
+
return this.#internals?.validity;
|
|
815
|
+
}
|
|
816
|
+
get validationMessage() {
|
|
817
|
+
return this.#internals?.validationMessage ?? "";
|
|
818
|
+
}
|
|
819
|
+
get willValidate() {
|
|
820
|
+
return this.#internals?.willValidate ?? false;
|
|
821
|
+
}
|
|
822
|
+
checkValidity() {
|
|
823
|
+
return this.#internals?.checkValidity() ?? true;
|
|
824
|
+
}
|
|
825
|
+
reportValidity() {
|
|
826
|
+
return this.#internals?.reportValidity() ?? true;
|
|
827
|
+
}
|
|
828
|
+
formResetCallback() {
|
|
829
|
+
this.#applyValue(this.getAttribute("defaultvalue") ?? "");
|
|
830
|
+
this.#iso = this.value;
|
|
831
|
+
this.#render();
|
|
832
|
+
this.#internals?.setFormValue(this.value);
|
|
833
|
+
this.#updateFilled();
|
|
834
|
+
this.#updateValidity();
|
|
835
|
+
this.#dispatch("input");
|
|
836
|
+
this.#dispatch("change");
|
|
837
|
+
}
|
|
838
|
+
formDisabledCallback(disabled) {
|
|
839
|
+
this.#formDisabled = disabled;
|
|
840
|
+
this.#syncDisabled();
|
|
841
|
+
}
|
|
842
|
+
formStateRestoreCallback(state) {
|
|
843
|
+
this.value = state ?? "";
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function register_a_input_time() {
|
|
847
|
+
if (typeof customElements === "undefined") return;
|
|
848
|
+
if (!customElements.get("a-input-time")) {
|
|
849
|
+
customElements.define("a-input-time", AInputTimeElement);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
register_a_input_time();
|
|
853
|
+
export {
|
|
854
|
+
AInputTimeElement,
|
|
855
|
+
register_a_input_time
|
|
856
|
+
};
|