@lilydesignsystem/angular-date-time-picker 0.1.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.
@@ -0,0 +1,2153 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { Directive, input, model, output, contentChild, TemplateRef, viewChild, inject, ChangeDetectorRef, signal, computed, effect, ChangeDetectionStrategy, Component } from '@angular/core';
4
+
5
+ /**
6
+ * Default button glyph: U+1F4C5 CALENDAR, followed by U+FE0E VARIATION
7
+ * SELECTOR-15 to request text (monochrome) presentation.
8
+ *
9
+ * Same construction as locale-picker's globe. The variation selector is a
10
+ * hint, not a guarantee — several platforms ignore it and render the emoji
11
+ * anyway — but where it is honoured the glyph inherits the page's text
12
+ * colour and stops looking like a sticker among the siblings' ◑, "A" and
13
+ * ➤. Written as an escape, never as a bare character: a variation
14
+ * selector has no visual form at all, so a bare one is invisible in an
15
+ * editor and trivially lost to a careless edit.
16
+ */
17
+ const CALENDAR = "📅︎";
18
+ // -----------------------------------------------------------------
19
+ // Civil-date arithmetic
20
+ //
21
+ // Pure and total: no local-time `Date` values, no throwing, null for
22
+ // "not a date". Exported so the acceptance tests can exercise the
23
+ // arithmetic directly rather than only through the rendered grid.
24
+ // -----------------------------------------------------------------
25
+ /** Zero-pad to `width`. */
26
+ function pad(n, width = 2) {
27
+ return String(Math.abs(n)).padStart(width, "0");
28
+ }
29
+ /** Days in a month. `month` is 1-12. */
30
+ function daysInMonth(year, month) {
31
+ // Day 0 of the next month is the last day of this one.
32
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
33
+ }
34
+ /** `{2026, 3, 1}` → `"2026-03-01"`. */
35
+ function formatIsoDate(date) {
36
+ return `${pad(date.year, 4)}-${pad(date.month)}-${pad(date.day)}`;
37
+ }
38
+ /**
39
+ * `"2026-03-01"` → `{2026, 3, 1}`, or null.
40
+ *
41
+ * Rejects impossible components rather than rolling them over, so
42
+ * `"2026-02-31"` is null and not the 3rd of March. A field that silently
43
+ * reinterprets an impossible date is worse than one that refuses it.
44
+ */
45
+ function parseIsoDate(text) {
46
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text.trim());
47
+ if (!m)
48
+ return null;
49
+ const year = Number(m[1]);
50
+ const month = Number(m[2]);
51
+ const day = Number(m[3]);
52
+ if (month < 1 || month > 12)
53
+ return null;
54
+ if (day < 1 || day > daysInMonth(year, month))
55
+ return null;
56
+ return { year, month, day };
57
+ }
58
+ /** Days since the Unix epoch. The unit all date arithmetic goes through. */
59
+ function toEpochDay(date) {
60
+ return Date.UTC(date.year, date.month - 1, date.day) / 86400000;
61
+ }
62
+ /** Inverse of `toEpochDay`. */
63
+ function fromEpochDay(epochDay) {
64
+ const d = new Date(epochDay * 86400000);
65
+ return {
66
+ year: d.getUTCFullYear(),
67
+ month: d.getUTCMonth() + 1,
68
+ day: d.getUTCDate(),
69
+ };
70
+ }
71
+ /** Shift an ISO date by whole days. */
72
+ function addDays(isoDate, days) {
73
+ const date = parseIsoDate(isoDate);
74
+ if (!date)
75
+ return isoDate;
76
+ return formatIsoDate(fromEpochDay(toEpochDay(date) + days));
77
+ }
78
+ /**
79
+ * Shift an ISO date by whole months, clamping the day.
80
+ *
81
+ * 31 January + 1 month is 28 February (29 in a leap year), not 3 March.
82
+ * Rolling over is what `Date.prototype.setMonth` does and it is almost
83
+ * never what someone paging a calendar means.
84
+ */
85
+ function addMonths(isoDate, months) {
86
+ const date = parseIsoDate(isoDate);
87
+ if (!date)
88
+ return isoDate;
89
+ const total = date.year * 12 + (date.month - 1) + months;
90
+ const year = Math.floor(total / 12);
91
+ const month = (((total % 12) + 12) % 12) + 1;
92
+ return formatIsoDate({
93
+ year,
94
+ month,
95
+ day: Math.min(date.day, daysInMonth(year, month)),
96
+ });
97
+ }
98
+ /** Day of week: 0 = Sunday … 6 = Saturday. */
99
+ function weekdayOf(isoDate) {
100
+ const date = parseIsoDate(isoDate);
101
+ if (!date)
102
+ return 0;
103
+ return new Date(Date.UTC(date.year, date.month - 1, date.day)).getUTCDay();
104
+ }
105
+ /**
106
+ * ISO-8601 week number.
107
+ *
108
+ * Weeks start Monday and week 1 is the one containing the first Thursday,
109
+ * which is why this pivots on Thursday rather than counting from 1
110
+ * January. Public-sector and clinical rotas quote ISO week numbers, so
111
+ * the column is worth getting exactly right.
112
+ */
113
+ function isoWeek(isoDate) {
114
+ if (!parseIsoDate(isoDate))
115
+ return 0;
116
+ const mondayIndex = (weekdayOf(isoDate) + 6) % 7;
117
+ const thursday = addDays(isoDate, 3 - mondayIndex);
118
+ const parsed = parseIsoDate(thursday);
119
+ if (!parsed)
120
+ return 0;
121
+ const jan1 = parseIsoDate(formatIsoDate({ year: parsed.year, month: 1, day: 1 }));
122
+ if (!jan1)
123
+ return 0;
124
+ return Math.floor((toEpochDay(parsed) - toEpochDay(jan1)) / 7) + 1;
125
+ }
126
+ /** `"09:30"` → `{9, 30}`, or null. */
127
+ function parseIsoTime(text) {
128
+ const m = /^(\d{2}):(\d{2})$/.exec(text.trim());
129
+ if (!m)
130
+ return null;
131
+ const hour = Number(m[1]);
132
+ const minute = Number(m[2]);
133
+ if (hour > 23 || minute > 59)
134
+ return null;
135
+ return { hour, minute };
136
+ }
137
+ /** `{9, 30}` → `"09:30"`. */
138
+ function formatIsoTime(time) {
139
+ return `${pad(time.hour)}:${pad(time.minute)}`;
140
+ }
141
+ /** Pull the date and time halves out of a mode-appropriate ISO value. */
142
+ function splitValue(value, mode) {
143
+ if (!value)
144
+ return { date: "", time: "" };
145
+ if (mode === "time") {
146
+ return { date: "", time: parseIsoTime(value) ? value : "" };
147
+ }
148
+ const [datePart = "", timePart = ""] = value.split("T");
149
+ return {
150
+ date: parseIsoDate(datePart) ? datePart : "",
151
+ time: mode === "datetime" && parseIsoTime(timePart) ? timePart : "",
152
+ };
153
+ }
154
+ /** Recombine the halves. Returns "" when the value is incomplete. */
155
+ function joinValue(date, time, mode) {
156
+ if (mode === "date")
157
+ return date;
158
+ if (mode === "time")
159
+ return time;
160
+ return date && time ? `${date}T${time}` : "";
161
+ }
162
+ /** Is `isoDate` inside the inclusive [min, max] window? Empty bounds pass. */
163
+ function withinRange(isoDate, min, max) {
164
+ if (min && isoDate < min)
165
+ return false;
166
+ if (max && isoDate > max)
167
+ return false;
168
+ return true;
169
+ }
170
+ /** Uppercase region subtag of a BCP 47 tag, or "". */
171
+ function regionOf(locale) {
172
+ if (!locale)
173
+ return "";
174
+ for (const part of locale.split(/[-_]/).slice(1)) {
175
+ if (/^[A-Za-z]{2}$/.test(part))
176
+ return part.toUpperCase();
177
+ }
178
+ return "";
179
+ }
180
+ const SUNDAY_FIRST_REGIONS = new Set([
181
+ "AR", "BR", "CA", "CL", "CO", "DO", "GT", "HK", "IL", "IN", "JP",
182
+ "KR", "MO", "MX", "PE", "PH", "PK", "TH", "TW", "US", "VE", "ZA",
183
+ ]);
184
+ const SATURDAY_FIRST_REGIONS = new Set([
185
+ "AE", "AF", "BH", "DJ", "DZ", "EG", "IQ", "IR", "JO", "KW", "LY",
186
+ "OM", "QA", "SA", "SD", "SY", "YE",
187
+ ]);
188
+ /**
189
+ * First day of the week for a locale: 0 = Sunday … 6 = Saturday.
190
+ *
191
+ * `Intl.Locale.prototype.getWeekInfo` is the right answer, but it is
192
+ * recent enough that a fallback still earns its place — and it is
193
+ * missing from some SSR runtimes entirely. The fallback is a short
194
+ * region table plus a Monday default, Monday being both the ISO-8601
195
+ * rule and the majority convention worldwide.
196
+ */
197
+ function firstDayOfWeekFor(locale) {
198
+ if (locale) {
199
+ try {
200
+ const loc = new Intl.Locale(locale);
201
+ const info = loc.getWeekInfo?.() ?? loc.weekInfo;
202
+ // getWeekInfo reports 1 = Monday … 7 = Sunday, so Sunday (7) has to
203
+ // fold to 0.
204
+ if (info && typeof info.firstDay === "number") {
205
+ return info.firstDay % 7;
206
+ }
207
+ }
208
+ catch {
209
+ // Malformed tag — fall through to the table.
210
+ }
211
+ }
212
+ const region = regionOf(locale);
213
+ if (SUNDAY_FIRST_REGIONS.has(region))
214
+ return 0;
215
+ if (SATURDAY_FIRST_REGIONS.has(region))
216
+ return 6;
217
+ return 1;
218
+ }
219
+ /**
220
+ * The dates of one month's grid, always six rows of seven.
221
+ *
222
+ * Fixed height on purpose. A grid sized to its month runs four to six
223
+ * rows, so the footer — and the confirm button in it — moves vertically
224
+ * as you page months. That is a Fitts's-law tax on every subsequent click
225
+ * and it makes the dialog jump under the pointer. A constant six rows
226
+ * costs at most one extra trailing week.
227
+ */
228
+ function monthMatrix(year, month, firstDayOfWeek) {
229
+ const first = formatIsoDate({ year, month, day: 1 });
230
+ const lead = (weekdayOf(first) - firstDayOfWeek + 7) % 7;
231
+ const start = addDays(first, -lead);
232
+ const weeks = [];
233
+ for (let row = 0; row < 6; row++) {
234
+ const week = [];
235
+ for (let col = 0; col < 7; col++) {
236
+ week.push(addDays(start, row * 7 + col));
237
+ }
238
+ weeks.push(week);
239
+ }
240
+ return weeks;
241
+ }
242
+ /** Long and short month names for a locale, index 0 = January. */
243
+ function monthNames(locale) {
244
+ const build = (month) => {
245
+ try {
246
+ const fmt = new Intl.DateTimeFormat(locale, {
247
+ month,
248
+ timeZone: "UTC",
249
+ });
250
+ return Array.from({ length: 12 }, (_, i) => fmt.format(new Date(Date.UTC(2021, i, 15))));
251
+ }
252
+ catch {
253
+ return [];
254
+ }
255
+ };
256
+ return { long: build("long"), short: build("short") };
257
+ }
258
+ /** Match a token against a locale's month names. Returns 1-12, or 0. */
259
+ function matchMonthName(token, names) {
260
+ const norm = (s) => s.toLocaleLowerCase().replace(/\.$/, "").normalize("NFKD");
261
+ const t = norm(token);
262
+ if (!t || /^\d+$/.test(t))
263
+ return 0;
264
+ for (let i = 0; i < 12; i++) {
265
+ if (norm(names.long[i] ?? "") === t)
266
+ return i + 1;
267
+ if (norm(names.short[i] ?? "") === t)
268
+ return i + 1;
269
+ }
270
+ // Prefix match, so "Sept" finds September. Three characters minimum:
271
+ // "Ma" cannot choose between March and May, and guessing there would be
272
+ // worse than refusing.
273
+ if (t.length >= 3) {
274
+ for (let i = 0; i < 12; i++) {
275
+ const long = norm(names.long[i] ?? "");
276
+ if (long && long.startsWith(t))
277
+ return i + 1;
278
+ }
279
+ }
280
+ return 0;
281
+ }
282
+ /**
283
+ * The order a locale writes a numeric date in — `["day","month","year"]`
284
+ * for en-GB, `["month","day","year"]` for en-US.
285
+ */
286
+ function numericFieldOrder(locale) {
287
+ try {
288
+ const parts = new Intl.DateTimeFormat(locale, {
289
+ day: "2-digit",
290
+ month: "2-digit",
291
+ year: "numeric",
292
+ timeZone: "UTC",
293
+ }).formatToParts(new Date(Date.UTC(2021, 4, 6)));
294
+ const order = parts
295
+ .map((p) => p.type)
296
+ .filter((t) => t === "day" || t === "month" || t === "year");
297
+ if (order.length === 3)
298
+ return order;
299
+ }
300
+ catch {
301
+ // Fall through.
302
+ }
303
+ return ["day", "month", "year"];
304
+ }
305
+ /**
306
+ * Parse typed text into an ISO date.
307
+ *
308
+ * Accepts, in order: ISO `YYYY-MM-DD`; a numeric form whose field order
309
+ * follows the locale, so `03/04/2026` is 3 April in en-GB and 4 March in
310
+ * en-US — which is what each user means; and a form with a written month
311
+ * matched against the locale's own long and short month names, which is
312
+ * what lets DHCW's `27-Jun-2025` round-trip.
313
+ *
314
+ * Anything else is null. Guessing harder than this is how date fields
315
+ * silently record the wrong day.
316
+ */
317
+ function parseDateInput(text, locale) {
318
+ const trimmed = text.trim();
319
+ if (!trimmed)
320
+ return null;
321
+ const iso = parseIsoDate(trimmed);
322
+ if (iso)
323
+ return formatIsoDate(iso);
324
+ const parts = trimmed.split(/[\s./-]+/).filter(Boolean);
325
+ if (parts.length !== 3)
326
+ return null;
327
+ // Look for a written month first. Whichever field it is, the other two
328
+ // are the day and the year, and their order is then decidable without
329
+ // knowing the locale: the larger number is the year.
330
+ const names = monthNames(locale);
331
+ let month = 0;
332
+ let monthIndex = -1;
333
+ for (let i = 0; i < parts.length; i++) {
334
+ const found = matchMonthName(parts[i], names);
335
+ if (found) {
336
+ month = found;
337
+ monthIndex = i;
338
+ break;
339
+ }
340
+ }
341
+ let day = 0;
342
+ let year = 0;
343
+ if (monthIndex >= 0) {
344
+ const rest = parts
345
+ .filter((_, i) => i !== monthIndex)
346
+ .map((p) => Number(p));
347
+ if (rest.some((n) => Number.isNaN(n)))
348
+ return null;
349
+ [day, year] = rest[0] > rest[1] ? [rest[1], rest[0]] : rest;
350
+ }
351
+ else {
352
+ const nums = parts.map((p) => Number(p));
353
+ if (nums.some((n) => Number.isNaN(n)))
354
+ return null;
355
+ const order = numericFieldOrder(locale);
356
+ year = nums[order.indexOf("year")];
357
+ day = nums[order.indexOf("day")];
358
+ month = nums[order.indexOf("month")];
359
+ }
360
+ // Two-digit years: the usual 70 pivot. Documented rather than clever,
361
+ // because any choice here is wrong for someone.
362
+ if (year < 100)
363
+ year += year < 70 ? 2000 : 1900;
364
+ if (month < 1 || month > 12)
365
+ return null;
366
+ if (day < 1 || day > daysInMonth(year, month))
367
+ return null;
368
+ return formatIsoDate({ year, month, day });
369
+ }
370
+ /** Accepts `9:30`, `09:30`, `0930`, `9.30`, and a trailing am/pm. */
371
+ function parseTimeInput(text) {
372
+ const m = /^(\d{1,2})[:.]?(\d{2})\s*([ap])\.?m\.?$|^(\d{1,2})[:.]?(\d{2})$/.exec(text.trim().toLowerCase());
373
+ if (!m)
374
+ return null;
375
+ let hour = Number(m[1] ?? m[4]);
376
+ const minute = Number(m[2] ?? m[5]);
377
+ const meridiem = m[3];
378
+ if (meridiem === "p" && hour < 12)
379
+ hour += 12;
380
+ if (meridiem === "a" && hour === 12)
381
+ hour = 0;
382
+ if (hour > 23 || minute > 59)
383
+ return null;
384
+ return formatIsoTime({ hour, minute });
385
+ }
386
+ /** Does this locale write times on a 12-hour clock? */
387
+ function localeUsesHour12(loc) {
388
+ try {
389
+ const parts = new Intl.DateTimeFormat(loc, {
390
+ hour: "numeric",
391
+ timeZone: "UTC",
392
+ }).formatToParts(new Date(Date.UTC(2021, 0, 1, 13)));
393
+ return parts.some((p) => p.type === "dayPeriod");
394
+ }
395
+ catch {
396
+ return false;
397
+ }
398
+ }
399
+ /** The host's current calendar day, read via the *local* getters. */
400
+ function todayIso() {
401
+ const now = new Date();
402
+ return formatIsoDate({
403
+ year: now.getFullYear(),
404
+ month: now.getMonth() + 1,
405
+ day: now.getDate(),
406
+ });
407
+ }
408
+ let uid = 0;
409
+ /** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */
410
+ function nextDateTimePickerId() {
411
+ uid += 1;
412
+ return `date-time-picker-${uid}`;
413
+ }
414
+ /**
415
+ * Optional marker for the projected icon template. Gives consumers typed
416
+ * `let-` variables:
417
+ *
418
+ * ```html
419
+ * <lily-date-time-picker label="Choose a date" [labels]="labels">
420
+ * <ng-template lilyDateTimePickerIcon let-args>{{ args.open ? "×" : "📅" }}</ng-template>
421
+ * </lily-date-time-picker>
422
+ * ```
423
+ *
424
+ * The component queries any projected `<ng-template>`, so the marker is
425
+ * for type-checking and readability, not for matching.
426
+ */
427
+ class DateTimePickerIcon {
428
+ static ngTemplateContextGuard(_dir, _ctx) {
429
+ return true;
430
+ }
431
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: DateTimePickerIcon, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
432
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: DateTimePickerIcon, isStandalone: true, selector: "ng-template[lilyDateTimePickerIcon]", ngImport: i0 }); }
433
+ }
434
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: DateTimePickerIcon, decorators: [{
435
+ type: Directive,
436
+ args: [{
437
+ selector: "ng-template[lilyDateTimePickerIcon]",
438
+ standalone: true,
439
+ }]
440
+ }] });
441
+ /**
442
+ * DateTimePicker — a headless date / time / datetime control.
443
+ *
444
+ * A typeable text field plus an icon button that opens a WAI-ARIA APG
445
+ * **Date Picker Dialog**: a month grid with a full keyboard contract.
446
+ * Locale-correct by construction — month names, weekday names, first day
447
+ * of week, numeric field order, 12- vs 24-hour clock and day-period names
448
+ * all come from `Intl`. See `spec/index.md` for the full contract.
449
+ *
450
+ * Unlike the three preference helpers this is a **form control**, not a
451
+ * page-header preference control, and it persists nothing.
452
+ */
453
+ class DateTimePicker {
454
+ constructor() {
455
+ /** Accessible name for the trigger button and the dialog. */
456
+ this.label = input.required(/* @ts-ignore */
457
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
458
+ /** Every other user-facing string. See `spec/index.md` §4.2. */
459
+ this.labels = input.required(/* @ts-ignore */
460
+ ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
461
+ /** What to collect: a date, a time, or both. */
462
+ this.mode = input("date", /* @ts-ignore */
463
+ ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
464
+ /** ISO value: `YYYY-MM-DD`, `HH:MM`, or `YYYY-MM-DDTHH:MM`. Two-way bindable. */
465
+ this.value = model("", /* @ts-ignore */
466
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
467
+ /** BCP 47 tag driving month names, weekday names and first weekday. */
468
+ this.locale = input("", /* @ts-ignore */
469
+ ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
470
+ /** Earliest selectable date, ISO. */
471
+ this.min = input("", /* @ts-ignore */
472
+ ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
473
+ /** Latest selectable date, ISO. */
474
+ this.max = input("", /* @ts-ignore */
475
+ ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
476
+ /** Veto individual dates — true means "cannot be picked". */
477
+ this.isDateDisabled = input(undefined, /* @ts-ignore */
478
+ ...(ngDevMode ? [{ debugName: "isDateDisabled" }] : /* istanbul ignore next */ []));
479
+ /** 0 = Sunday … 6 = Saturday. Defaults to the locale's convention. */
480
+ this.firstDayOfWeek = input(undefined, /* @ts-ignore */
481
+ ...(ngDevMode ? [{ debugName: "firstDayOfWeek" }] : /* istanbul ignore next */ []));
482
+ /** Granularity of the minute select. */
483
+ this.minuteStep = input(1, /* @ts-ignore */
484
+ ...(ngDevMode ? [{ debugName: "minuteStep" }] : /* istanbul ignore next */ []));
485
+ /** 12-hour clock. Defaults to the locale's convention. */
486
+ this.hour12 = input(undefined, /* @ts-ignore */
487
+ ...(ngDevMode ? [{ debugName: "hour12" }] : /* istanbul ignore next */ []));
488
+ /** Render an ISO-8601 week-number column. */
489
+ this.showWeekNumbers = input(false, /* @ts-ignore */
490
+ ...(ngDevMode ? [{ debugName: "showWeekNumbers" }] : /* istanbul ignore next */ []));
491
+ /** Quick-pick buttons. */
492
+ this.shortcuts = input([], /* @ts-ignore */
493
+ ...(ngDevMode ? [{ debugName: "shortcuts" }] : /* istanbul ignore next */ []));
494
+ /** Commit and close as soon as a day is chosen. Defaults to date-only. */
495
+ this.confirmOnSelect = input(undefined, /* @ts-ignore */
496
+ ...(ngDevMode ? [{ debugName: "confirmOnSelect" }] : /* istanbul ignore next */ []));
497
+ /** `name` of the hidden input that carries the value in a form post. */
498
+ this.name = input("date-time", /* @ts-ignore */
499
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
500
+ /**
501
+ * Selected IANA time zone (e.g. `Europe/London`), or `""` for none.
502
+ * Two-way bindable via `[(timeZone)]`, matching `value`. Rides its own
503
+ * hidden input `{name}-time-zone` and is reflected as `data-time-zone`
504
+ * on the root. It is metadata about WHERE the civil value applies, not
505
+ * part of the value — converting to an instant stays the consumer's
506
+ * job. Never guessed from the runtime: the picker no more picks a zone
507
+ * than `locale-picker` picks a locale.
508
+ */
509
+ this.timeZone = model("", /* @ts-ignore */
510
+ ...(ngDevMode ? [{ debugName: "timeZone" }] : /* istanbul ignore next */ []));
511
+ /**
512
+ * Zones offered by the select. Defaults to every zone the runtime
513
+ * knows via `Intl.supportedValuesOf("timeZone")` — never a bundled
514
+ * table, the rule month and weekday names already follow.
515
+ */
516
+ this.timeZones = input(undefined, /* @ts-ignore */
517
+ ...(ngDevMode ? [{ debugName: "timeZones" }] : /* istanbul ignore next */ []));
518
+ /** Display text per zone id; a zone without an entry shows its id. */
519
+ this.timeZoneLabels = input({}, /* @ts-ignore */
520
+ ...(ngDevMode ? [{ debugName: "timeZoneLabels" }] : /* istanbul ignore next */ []));
521
+ /** `id` of the text field, so a consumer `<label for>` can name it. */
522
+ this.inputId = input("", /* @ts-ignore */
523
+ ...(ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []));
524
+ /** Forwarded to the text field as `aria-describedby`. */
525
+ this.describedBy = input("", /* @ts-ignore */
526
+ ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
527
+ /** Placeholder for the text field. */
528
+ this.placeholder = input("", /* @ts-ignore */
529
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
530
+ /** Disable the whole control. */
531
+ this.disabled = input(false, /* @ts-ignore */
532
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
533
+ /**
534
+ * Show the value but refuse edits.
535
+ *
536
+ * Named `readOnly` (not `readonly`) so the Angular input has an
537
+ * unambiguous identifier distinct from the TypeScript `readonly`
538
+ * modifier — the one Angular-forced rename beyond `class` → `className`.
539
+ */
540
+ this.readOnly = input(false, /* @ts-ignore */
541
+ ...(ngDevMode ? [{ debugName: "readOnly" }] : /* istanbul ignore next */ []));
542
+ /** Mark the field required. */
543
+ this.required = input(false, /* @ts-ignore */
544
+ ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
545
+ /** Override how a committed value is rendered into the text field. */
546
+ this.formatValue = input(undefined, /* @ts-ignore */
547
+ ...(ngDevMode ? [{ debugName: "formatValue" }] : /* istanbul ignore next */ []));
548
+ /** Override how typed text is turned back into an ISO value. */
549
+ this.parseInput = input(undefined, /* @ts-ignore */
550
+ ...(ngDevMode ? [{ debugName: "parseInput" }] : /* istanbul ignore next */ []));
551
+ /** Extra CSS class on the root `<div>`. */
552
+ this.className = input("", /* @ts-ignore */
553
+ ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
554
+ /** Fires after a new value is committed. */
555
+ this.change = output();
556
+ /** Fires when a shortcut is used, before the value settles. */
557
+ this.shortcut = output();
558
+ /** Fires when typed text cannot be parsed. */
559
+ this.invalidInput = output();
560
+ /** Projected icon template; replaces the default glyph when supplied. */
561
+ this.iconTemplate = contentChild(TemplateRef, /* @ts-ignore */
562
+ ...(ngDevMode ? [{ debugName: "iconTemplate" }] : /* istanbul ignore next */ []));
563
+ this.rootRef = viewChild.required("rootEl", /* @ts-ignore */
564
+ ...(ngDevMode ? [{ debugName: "rootRef" }] : /* istanbul ignore next */ []));
565
+ this.buttonRef = viewChild.required("buttonEl", /* @ts-ignore */
566
+ ...(ngDevMode ? [{ debugName: "buttonRef" }] : /* istanbul ignore next */ []));
567
+ this.dialogRef = viewChild.required("dialogEl", /* @ts-ignore */
568
+ ...(ngDevMode ? [{ debugName: "dialogRef" }] : /* istanbul ignore next */ []));
569
+ /** Optional: the grid renders only when `mode` includes a date. */
570
+ this.gridRef = viewChild("gridEl", /* @ts-ignore */
571
+ ...(ngDevMode ? [{ debugName: "gridRef" }] : /* istanbul ignore next */ []));
572
+ this.glyph = CALENDAR;
573
+ /**
574
+ * For rendering the view synchronously before a focus move.
575
+ *
576
+ * Svelte updates the DOM synchronously when state changes, so the
577
+ * canonical helper can focus a freshly-paged month from a microtask.
578
+ * Angular renders later — after the microtask queue drains, whether the
579
+ * app is zone-based or zoneless — so a queued focus into a month that
580
+ * has not rendered yet finds nothing (or, worse, finds and focuses a
581
+ * cell that the next render destroys, dropping focus to `<body>`).
582
+ * `cdr.detectChanges()` inside the event handler is the Angular
583
+ * equivalent of Svelte's synchronous flush.
584
+ */
585
+ this.cdr = inject(ChangeDetectorRef);
586
+ this.baseId = nextDateTimePickerId();
587
+ this.dialogId = `${this.baseId}-dialog`;
588
+ this.periodId = `${this.baseId}-period`;
589
+ this.hourId = `${this.baseId}-hour`;
590
+ this.minuteId = `${this.baseId}-minute`;
591
+ this.meridiemId = `${this.baseId}-meridiem`;
592
+ this.statusId = `${this.baseId}-status`;
593
+ this.instructionsId = `${this.baseId}-instructions`;
594
+ this.timeZoneId = `${this.baseId}-time-zone`;
595
+ this.open = signal(false, /* @ts-ignore */
596
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
597
+ this.invalid = signal(false, /* @ts-ignore */
598
+ ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
599
+ /**
600
+ * The element that opened the dialog, so close can return focus to it.
601
+ *
602
+ * The APG rule is "focus returns to the element that invoked the
603
+ * dialog" — which is the trigger button on a click, but the *text
604
+ * field* when the user pressed Alt+ArrowDown. Always refocusing the
605
+ * button strands a keyboard user one Tab stop past where they were.
606
+ */
607
+ this.openerEl = null;
608
+ /**
609
+ * Text the user has typed but not yet resolved.
610
+ *
611
+ * `null` means "no pending edit, show the formatted value". An empty
612
+ * string is a real state (the user cleared the field) and must not
613
+ * collapse into `null`.
614
+ */
615
+ this.typed = signal(null, /* @ts-ignore */
616
+ ...(ngDevMode ? [{ debugName: "typed" }] : /* istanbul ignore next */ []));
617
+ /**
618
+ * The pending selection, live only while the dialog is open.
619
+ *
620
+ * Kept separate from `value` so Cancel and Escape can genuinely revert.
621
+ * A picker that writes through on every arrow key has no cancel — only
622
+ * an undo the user has to perform by hand, from memory.
623
+ */
624
+ this.pendingDate = signal("", /* @ts-ignore */
625
+ ...(ngDevMode ? [{ debugName: "pendingDate" }] : /* istanbul ignore next */ []));
626
+ this.pendingTime = signal("", /* @ts-ignore */
627
+ ...(ngDevMode ? [{ debugName: "pendingTime" }] : /* istanbul ignore next */ []));
628
+ /** The month on screen. */
629
+ this.viewYear = signal(1970, /* @ts-ignore */
630
+ ...(ngDevMode ? [{ debugName: "viewYear" }] : /* istanbul ignore next */ []));
631
+ this.viewMonth = signal(1, /* @ts-ignore */
632
+ ...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
633
+ /** The day the grid's roving tabindex sits on. */
634
+ this.cursor = signal("", /* @ts-ignore */
635
+ ...(ngDevMode ? [{ debugName: "cursor" }] : /* istanbul ignore next */ []));
636
+ /**
637
+ * Today, sampled once at open and refreshed on the initial effect below.
638
+ * Not re-read per cell: the grid asks "is this today?" 42 times a
639
+ * render, and a clock read per cell buys nothing over a clock read per
640
+ * open.
641
+ */
642
+ this.today = signal("", /* @ts-ignore */
643
+ ...(ngDevMode ? [{ debugName: "today" }] : /* istanbul ignore next */ []));
644
+ this.fieldId = computed(() => this.inputId() || `${this.baseId}-input`, /* @ts-ignore */
645
+ ...(ngDevMode ? [{ debugName: "fieldId" }] : /* istanbul ignore next */ []));
646
+ this.committed = computed(() => splitValue(this.value(), this.mode()), /* @ts-ignore */
647
+ ...(ngDevMode ? [{ debugName: "committed" }] : /* istanbul ignore next */ []));
648
+ this.weekStart = computed(() => this.firstDayOfWeek() ?? firstDayOfWeekFor(this.locale() || undefined), /* @ts-ignore */
649
+ ...(ngDevMode ? [{ debugName: "weekStart" }] : /* istanbul ignore next */ []));
650
+ this.commitOnDay = computed(() => this.confirmOnSelect() ?? this.mode() === "date", /* @ts-ignore */
651
+ ...(ngDevMode ? [{ debugName: "commitOnDay" }] : /* istanbul ignore next */ []));
652
+ this.usesDate = computed(() => this.mode() !== "time", /* @ts-ignore */
653
+ ...(ngDevMode ? [{ debugName: "usesDate" }] : /* istanbul ignore next */ []));
654
+ this.usesTime = computed(() => this.mode() !== "date", /* @ts-ignore */
655
+ ...(ngDevMode ? [{ debugName: "usesTime" }] : /* istanbul ignore next */ []));
656
+ this.clock12 = computed(() => this.hour12() ?? localeUsesHour12(this.locale() || undefined), /* @ts-ignore */
657
+ ...(ngDevMode ? [{ debugName: "clock12" }] : /* istanbul ignore next */ []));
658
+ /**
659
+ * `aria-describedby` for the field: the consumer's hint, plus — while
660
+ * invalid, with `labels().invalid` supplied — the status region, for
661
+ * the assistive technologies that read `aria-describedby` but not the
662
+ * newer `aria-errormessage`.
663
+ */
664
+ this.fieldDescribedBy = computed(() => [
665
+ this.describedBy() || undefined,
666
+ this.invalid() && this.labels().invalid ? this.statusId : undefined,
667
+ ]
668
+ .filter(Boolean)
669
+ .join(" ") || null, /* @ts-ignore */
670
+ ...(ngDevMode ? [{ debugName: "fieldDescribedBy" }] : /* istanbul ignore next */ []));
671
+ /** The text shown in the field. A pending edit wins until resolved. */
672
+ this.display = computed(() => {
673
+ const t = this.typed();
674
+ if (t !== null)
675
+ return t;
676
+ const formatter = this.formatValue() ?? ((v) => this.defaultFormat(v));
677
+ return formatter(this.value());
678
+ }, /* @ts-ignore */
679
+ ...(ngDevMode ? [{ debugName: "display" }] : /* istanbul ignore next */ []));
680
+ /** The "March 2026" heading. */
681
+ /**
682
+ * `Intl.supportedValuesOf` is guarded rather than called bare: an empty
683
+ * select beats a throw at mount on an older embedded runtime that
684
+ * lacks the method entirely.
685
+ */
686
+ this.zoneOptions = computed(() => {
687
+ if (this.timeZones() !== undefined)
688
+ return this.timeZones();
689
+ return typeof Intl.supportedValuesOf === "function"
690
+ ? Intl.supportedValuesOf("timeZone")
691
+ : [];
692
+ }, /* @ts-ignore */
693
+ ...(ngDevMode ? [{ debugName: "zoneOptions" }] : /* istanbul ignore next */ []));
694
+ this.periodText = computed(() => {
695
+ const viewYear = this.viewYear();
696
+ const viewMonth = this.viewMonth();
697
+ try {
698
+ return new Intl.DateTimeFormat(this.locale() || undefined, {
699
+ month: "long",
700
+ year: "numeric",
701
+ timeZone: "UTC",
702
+ }).format(new Date(Date.UTC(viewYear, viewMonth - 1, 1)));
703
+ }
704
+ catch {
705
+ return `${viewYear}-${pad(viewMonth)}`;
706
+ }
707
+ }, /* @ts-ignore */
708
+ ...(ngDevMode ? [{ debugName: "periodText" }] : /* istanbul ignore next */ []));
709
+ /** Column headings, in this locale, starting on `weekStart`. */
710
+ this.weekdays = computed(() => {
711
+ const weekStart = this.weekStart();
712
+ const locale = this.locale() || undefined;
713
+ const out = [];
714
+ for (let i = 0; i < 7; i++) {
715
+ // 2021-08-01 was a Sunday, so this walks the week from whichever
716
+ // day the locale starts on.
717
+ const d = new Date(Date.UTC(2021, 7, 1 + ((weekStart + i) % 7)));
718
+ try {
719
+ out.push({
720
+ short: new Intl.DateTimeFormat(locale, {
721
+ weekday: "short",
722
+ timeZone: "UTC",
723
+ }).format(d),
724
+ long: new Intl.DateTimeFormat(locale, {
725
+ weekday: "long",
726
+ timeZone: "UTC",
727
+ }).format(d),
728
+ });
729
+ }
730
+ catch {
731
+ out.push({ short: "", long: "" });
732
+ }
733
+ }
734
+ return out;
735
+ }, /* @ts-ignore */
736
+ ...(ngDevMode ? [{ debugName: "weekdays" }] : /* istanbul ignore next */ []));
737
+ this.weeks = computed(() => monthMatrix(this.viewYear(), this.viewMonth(), this.weekStart()), /* @ts-ignore */
738
+ ...(ngDevMode ? [{ debugName: "weeks" }] : /* istanbul ignore next */ []));
739
+ this.pendingHour = computed(() => parseIsoTime(this.pendingTime())?.hour ?? 0, /* @ts-ignore */
740
+ ...(ngDevMode ? [{ debugName: "pendingHour" }] : /* istanbul ignore next */ []));
741
+ this.pendingMinute = computed(() => parseIsoTime(this.pendingTime())?.minute ?? 0, /* @ts-ignore */
742
+ ...(ngDevMode ? [{ debugName: "pendingMinute" }] : /* istanbul ignore next */ []));
743
+ /**
744
+ * The hours offered.
745
+ *
746
+ * On a 12-hour clock this lists only the half of the day the meridiem
747
+ * select is currently on, so the two controls together name exactly one
748
+ * hour. Listing 1-12 once and letting the meridiem reinterpret it is
749
+ * the same thing, but it makes "12 AM" ambiguous at precisely the point
750
+ * where users already get it wrong.
751
+ */
752
+ this.hourOptions = computed(() => {
753
+ const clock12 = this.clock12();
754
+ const pendingHour = this.pendingHour();
755
+ const out = [];
756
+ for (let h = 0; h < 24; h++) {
757
+ if (clock12 && h < 12 !== pendingHour < 12)
758
+ continue;
759
+ out.push({
760
+ value: h,
761
+ label: clock12 ? String(((h + 11) % 12) + 1) : pad(h),
762
+ });
763
+ }
764
+ return out;
765
+ }, /* @ts-ignore */
766
+ ...(ngDevMode ? [{ debugName: "hourOptions" }] : /* istanbul ignore next */ []));
767
+ this.minuteOptions = computed(() => {
768
+ const step = Math.max(1, this.minuteStep());
769
+ const out = [];
770
+ for (let m = 0; m < 60; m += step)
771
+ out.push(m);
772
+ return out;
773
+ }, /* @ts-ignore */
774
+ ...(ngDevMode ? [{ debugName: "minuteOptions" }] : /* istanbul ignore next */ []));
775
+ this.childContext = computed(() => {
776
+ const args = {
777
+ value: this.value(),
778
+ open: this.open(),
779
+ display: this.display(),
780
+ };
781
+ return { $implicit: args, ...args };
782
+ }, /* @ts-ignore */
783
+ ...(ngDevMode ? [{ debugName: "childContext" }] : /* istanbul ignore next */ []));
784
+ this.initialised = false;
785
+ // Seeds the view month and the grid cursor from the committed value
786
+ // (or today) exactly once. Deferred to `effect()`, rather than read
787
+ // synchronously in the constructor, because signal inputs are not
788
+ // bound yet when the constructor runs — an Angular-forced difference
789
+ // from the Svelte original, which reads `value` synchronously at
790
+ // `$props()` time. `effect()` is this catalog's established seam for
791
+ // side effects driven by input signals; see catalog `AGENTS.md`.
792
+ effect(() => {
793
+ const committed = this.committed();
794
+ if (this.initialised)
795
+ return;
796
+ this.initialised = true;
797
+ const today = todayIso();
798
+ this.today.set(today);
799
+ const anchor = parseIsoDate(committed.date) ?? parseIsoDate(today);
800
+ if (anchor) {
801
+ this.viewYear.set(anchor.year);
802
+ this.viewMonth.set(anchor.month);
803
+ this.cursor.set(formatIsoDate(anchor));
804
+ }
805
+ });
806
+ }
807
+ // ---------------------------------------------------------------
808
+ // Template helpers
809
+ // ---------------------------------------------------------------
810
+ pad2(n) {
811
+ return pad(n);
812
+ }
813
+ isoWeekOf(isoDate) {
814
+ return isoWeek(isoDate);
815
+ }
816
+ /** Per-cell derived values for the grid template's `@let`. */
817
+ parseCell(isoDate) {
818
+ const parsed = parseIsoDate(isoDate);
819
+ return {
820
+ day: parsed ? String(parsed.day) : "",
821
+ outside: parsed ? parsed.month !== this.viewMonth() : false,
822
+ };
823
+ }
824
+ /** Accessible name for one day cell, e.g. "Sunday 1 March 2026". */
825
+ dayLabel(isoDate) {
826
+ const parsed = parseIsoDate(isoDate);
827
+ if (!parsed)
828
+ return isoDate;
829
+ try {
830
+ return new Intl.DateTimeFormat(this.locale() || undefined, {
831
+ weekday: "long",
832
+ day: "numeric",
833
+ month: "long",
834
+ year: "numeric",
835
+ timeZone: "UTC",
836
+ }).format(new Date(Date.UTC(parsed.year, parsed.month - 1, parsed.day)));
837
+ }
838
+ catch {
839
+ return isoDate;
840
+ }
841
+ }
842
+ /** The locale's own AM / PM strings, so neither is hardcoded. */
843
+ dayPeriodName(pm) {
844
+ try {
845
+ const parts = new Intl.DateTimeFormat(this.locale() || undefined, {
846
+ hour: "numeric",
847
+ hour12: true,
848
+ timeZone: "UTC",
849
+ }).formatToParts(new Date(Date.UTC(2021, 0, 1, pm ? 13 : 1)));
850
+ const found = parts.find((p) => p.type === "dayPeriod")?.value;
851
+ if (found)
852
+ return found;
853
+ }
854
+ catch {
855
+ // Fall through.
856
+ }
857
+ return pm ? "PM" : "AM";
858
+ }
859
+ formatTimeForDisplay(isoTime) {
860
+ const parsed = parseIsoTime(isoTime);
861
+ if (!parsed)
862
+ return isoTime;
863
+ try {
864
+ return new Intl.DateTimeFormat(this.locale() || undefined, {
865
+ hour: "2-digit",
866
+ minute: "2-digit",
867
+ hour12: this.clock12(),
868
+ timeZone: "UTC",
869
+ }).format(new Date(Date.UTC(2021, 0, 1, parsed.hour, parsed.minute)));
870
+ }
871
+ catch {
872
+ return isoTime;
873
+ }
874
+ }
875
+ /** Render an ISO value the way this locale writes it. */
876
+ defaultFormat(isoValue) {
877
+ if (!isoValue)
878
+ return "";
879
+ const { date, time } = splitValue(isoValue, this.mode());
880
+ const chunks = [];
881
+ const parsed = date ? parseIsoDate(date) : null;
882
+ if (parsed) {
883
+ try {
884
+ chunks.push(new Intl.DateTimeFormat(this.locale() || undefined, {
885
+ day: "2-digit",
886
+ month: "short",
887
+ year: "numeric",
888
+ timeZone: "UTC",
889
+ }).format(new Date(Date.UTC(parsed.year, parsed.month - 1, parsed.day))));
890
+ }
891
+ catch {
892
+ chunks.push(date);
893
+ }
894
+ }
895
+ if (time)
896
+ chunks.push(this.formatTimeForDisplay(time));
897
+ return chunks.join(" ");
898
+ }
899
+ // ---------------------------------------------------------------
900
+ // Selectability
901
+ // ---------------------------------------------------------------
902
+ dayDisabled(isoDate) {
903
+ if (!withinRange(isoDate, this.min() || undefined, this.max() || undefined)) {
904
+ return true;
905
+ }
906
+ return this.isDateDisabled()?.(isoDate) === true;
907
+ }
908
+ /** The nearest selectable day to `isoDate`, searching outwards. */
909
+ nearestSelectable(isoDate) {
910
+ if (!this.dayDisabled(isoDate))
911
+ return isoDate;
912
+ // Bounded: a year either way is far beyond any real min/max window,
913
+ // and an unbounded search would hang on a predicate that disables
914
+ // everything.
915
+ for (let delta = 1; delta <= 366; delta++) {
916
+ const after = addDays(isoDate, delta);
917
+ if (!this.dayDisabled(after))
918
+ return after;
919
+ const before = addDays(isoDate, -delta);
920
+ if (!this.dayDisabled(before))
921
+ return before;
922
+ }
923
+ return isoDate;
924
+ }
925
+ // ---------------------------------------------------------------
926
+ // Open / close / commit
927
+ // ---------------------------------------------------------------
928
+ openDialog() {
929
+ if (this.disabled() || this.readOnly())
930
+ return;
931
+ // Remember what opened the dialog — the field on Alt+ArrowDown, the
932
+ // trigger button on a click — so closeDialog can put focus back
933
+ // there, per the APG dialog rule.
934
+ const active = document.activeElement;
935
+ this.openerEl =
936
+ active instanceof HTMLElement &&
937
+ this.rootRef().nativeElement.contains(active)
938
+ ? active
939
+ : (this.buttonRef().nativeElement ?? null);
940
+ const today = todayIso();
941
+ this.today.set(today);
942
+ const pendingDate = this.committed().date || this.nearestSelectable(today);
943
+ this.pendingDate.set(pendingDate);
944
+ this.pendingTime.set(this.committed().time || this.defaultTime());
945
+ this.cursor.set(pendingDate);
946
+ const anchor = parseIsoDate(pendingDate) ?? parseIsoDate(today);
947
+ if (anchor) {
948
+ this.viewYear.set(anchor.year);
949
+ this.viewMonth.set(anchor.month);
950
+ }
951
+ this.open.set(true);
952
+ // Render first, focus second: until change detection runs, the
953
+ // dialog still carries `hidden`, and focusing into a hidden subtree
954
+ // is a no-op in a real browser.
955
+ this.cdr.detectChanges();
956
+ this.focusInitial();
957
+ }
958
+ /** Where an unset time starts: now, snapped down to the step. */
959
+ defaultTime() {
960
+ if (!this.usesTime())
961
+ return "";
962
+ const now = new Date();
963
+ const step = Math.max(1, this.minuteStep());
964
+ return formatIsoTime({
965
+ hour: now.getHours(),
966
+ minute: Math.floor(now.getMinutes() / step) * step,
967
+ });
968
+ }
969
+ /** Focus the grid cursor, or the first control when there is no grid. */
970
+ focusInitial() {
971
+ if (this.usesDate()) {
972
+ this.focusCursor();
973
+ return;
974
+ }
975
+ this.focusables()[0]?.focus?.();
976
+ }
977
+ closeDialog(refocus = true) {
978
+ if (!this.open())
979
+ return;
980
+ this.open.set(false);
981
+ if (refocus) {
982
+ const target = this.openerEl ?? this.buttonRef().nativeElement;
983
+ queueMicrotask(() => target?.focus?.());
984
+ }
985
+ }
986
+ /** Commit the pending selection to `value` and notify. */
987
+ commit() {
988
+ const next = joinValue(this.pendingDate(), this.pendingTime(), this.mode());
989
+ // An incomplete datetime is not committed. Half a timestamp is not a
990
+ // smaller truth; it is a different one.
991
+ if (!next)
992
+ return;
993
+ this.typed.set(null);
994
+ this.invalid.set(false);
995
+ if (next !== this.value()) {
996
+ this.value.set(next);
997
+ this.change.emit(next);
998
+ }
999
+ this.closeDialog();
1000
+ }
1001
+ clear() {
1002
+ this.typed.set(null);
1003
+ this.invalid.set(false);
1004
+ if (this.value() !== "") {
1005
+ this.value.set("");
1006
+ this.change.emit("");
1007
+ }
1008
+ this.closeDialog();
1009
+ }
1010
+ // ---------------------------------------------------------------
1011
+ // Grid navigation
1012
+ // ---------------------------------------------------------------
1013
+ /**
1014
+ * Move the cursor, paging the view when the target is off-screen.
1015
+ *
1016
+ * Disabled days are still reachable — the cursor lands on them and the
1017
+ * button is `aria-disabled` (so it takes real focus and announces), so
1018
+ * arrowing across a blocked range works. What is refused is leaving
1019
+ * the min/max window entirely, because there is nothing out there to
1020
+ * navigate to.
1021
+ */
1022
+ moveCursor(nextIso) {
1023
+ if (!withinRange(nextIso, this.min() || undefined, this.max() || undefined)) {
1024
+ return;
1025
+ }
1026
+ this.cursor.set(nextIso);
1027
+ const parsed = parseIsoDate(nextIso);
1028
+ if (parsed &&
1029
+ (parsed.year !== this.viewYear() || parsed.month !== this.viewMonth())) {
1030
+ this.viewYear.set(parsed.year);
1031
+ this.viewMonth.set(parsed.month);
1032
+ }
1033
+ // Synchronous render-then-focus: when the cursor crossed into a new
1034
+ // month, the cell it needs does not exist until change detection has
1035
+ // run — see `cdr`.
1036
+ this.cdr.detectChanges();
1037
+ this.focusCursor();
1038
+ }
1039
+ focusCursor() {
1040
+ const dialogEl = this.dialogRef()?.nativeElement;
1041
+ const cursor = this.cursor();
1042
+ if (!dialogEl || !cursor)
1043
+ return;
1044
+ // An attribute selector, not `#id`: the id is not on this element, and
1045
+ // a value we generated as `YYYY-MM-DD` needs no escaping.
1046
+ const el = dialogEl.querySelector(`[data-date="${cursor}"]`);
1047
+ // Guard the METHODS, not only the element. jsdom implements no
1048
+ // `scrollIntoView`, and an unguarded call throws from inside a
1049
+ // keydown handler — where it stays invisible to a green suite.
1050
+ el?.focus?.();
1051
+ el?.scrollIntoView?.({ block: "nearest" });
1052
+ }
1053
+ selectDay(isoDate) {
1054
+ if (this.dayDisabled(isoDate))
1055
+ return;
1056
+ this.pendingDate.set(isoDate);
1057
+ this.cursor.set(isoDate);
1058
+ if (this.commitOnDay())
1059
+ this.commit();
1060
+ }
1061
+ shiftMonth(delta) {
1062
+ const anchor = formatIsoDate({
1063
+ year: this.viewYear(),
1064
+ month: this.viewMonth(),
1065
+ day: 1,
1066
+ });
1067
+ const next = parseIsoDate(addMonths(anchor, delta));
1068
+ if (!next)
1069
+ return;
1070
+ // Refocus the cursor only when focus is already in the grid: grid
1071
+ // paging (PageUp/PageDown, or a browser that focused nothing on
1072
+ // click) must carry focus, or it dies with the unrendered cell —
1073
+ // but a header button must keep focus, or its user is yanked into
1074
+ // the grid after one activation and cannot page twice.
1075
+ const hadGridFocus = this.gridRef()?.nativeElement.contains(document.activeElement) === true;
1076
+ this.viewYear.set(next.year);
1077
+ this.viewMonth.set(next.month);
1078
+ // Carry the cursor into the new month rather than leaving the roving
1079
+ // tabindex on a cell that is no longer rendered.
1080
+ const c = parseIsoDate(this.cursor());
1081
+ if (c) {
1082
+ this.cursor.set(formatIsoDate({
1083
+ year: next.year,
1084
+ month: next.month,
1085
+ day: Math.min(c.day, daysInMonth(next.year, next.month)),
1086
+ }));
1087
+ if (hadGridFocus) {
1088
+ // Render-then-focus, synchronously — the target cell does not
1089
+ // exist until change detection has run. See `cdr`.
1090
+ this.cdr.detectChanges();
1091
+ this.focusCursor();
1092
+ }
1093
+ }
1094
+ }
1095
+ shiftYear(delta) {
1096
+ this.shiftMonth(delta * 12);
1097
+ }
1098
+ /**
1099
+ * Week/day steps are the fine end of the header: unlike month/year,
1100
+ * which move the GRID and merely carry the cursor, these move the
1101
+ * pending day itself by ±7 / ±1 civil days and page the grid only
1102
+ * when the new day leaves the shown month. A step off the min/max
1103
+ * window is refused outright; a step onto a vetoed day moves the
1104
+ * cursor — vetoed days are reachable, as with the arrow keys — but
1105
+ * leaves the pending selection where it was. No commit even under
1106
+ * `confirmOnSelect`.
1107
+ */
1108
+ shiftDays(delta) {
1109
+ const from = parseIsoDate(this.cursor()) ? this.cursor() : this.pendingDate();
1110
+ if (!from)
1111
+ return;
1112
+ const next = addDays(from, delta);
1113
+ if (!withinRange(next, this.min() || undefined, this.max() || undefined)) {
1114
+ return;
1115
+ }
1116
+ const hadGridFocus = this.gridRef()?.nativeElement.contains(document.activeElement) === true;
1117
+ const parsed = parseIsoDate(next);
1118
+ if (parsed &&
1119
+ (parsed.year !== this.viewYear() || parsed.month !== this.viewMonth())) {
1120
+ this.viewYear.set(parsed.year);
1121
+ this.viewMonth.set(parsed.month);
1122
+ }
1123
+ this.cursor.set(next);
1124
+ if (!this.dayDisabled(next))
1125
+ this.pendingDate.set(next);
1126
+ if (hadGridFocus) {
1127
+ this.cdr.detectChanges();
1128
+ this.focusCursor();
1129
+ }
1130
+ }
1131
+ onTimeZoneSelect(event) {
1132
+ this.timeZone.set(event.target.value);
1133
+ }
1134
+ /**
1135
+ * A plain index expression (`timeZoneLabels()[zone] ?? zone`) trips
1136
+ * NG8102: without `noUncheckedIndexedAccess`, TypeScript types a
1137
+ * `Record<string, string>` index as always `string`, so the compiler
1138
+ * sees the `??` as dead code even though a real, unlisted zone id is
1139
+ * exactly the case it exists for. The cast documents that the index
1140
+ * genuinely can miss.
1141
+ */
1142
+ zoneLabel(zone) {
1143
+ return (this.timeZoneLabels()[zone] ?? zone);
1144
+ }
1145
+ onGridKeydown(event) {
1146
+ switch (event.key) {
1147
+ case "ArrowLeft":
1148
+ event.preventDefault();
1149
+ this.moveCursor(addDays(this.cursor(), -1));
1150
+ break;
1151
+ case "ArrowRight":
1152
+ event.preventDefault();
1153
+ this.moveCursor(addDays(this.cursor(), 1));
1154
+ break;
1155
+ case "ArrowUp":
1156
+ event.preventDefault();
1157
+ this.moveCursor(addDays(this.cursor(), -7));
1158
+ break;
1159
+ case "ArrowDown":
1160
+ event.preventDefault();
1161
+ this.moveCursor(addDays(this.cursor(), 7));
1162
+ break;
1163
+ case "Home": {
1164
+ event.preventDefault();
1165
+ const offset = (weekdayOf(this.cursor()) - this.weekStart() + 7) % 7;
1166
+ this.moveCursor(addDays(this.cursor(), -offset));
1167
+ break;
1168
+ }
1169
+ case "End": {
1170
+ event.preventDefault();
1171
+ const offset = (weekdayOf(this.cursor()) - this.weekStart() + 7) % 7;
1172
+ this.moveCursor(addDays(this.cursor(), 6 - offset));
1173
+ break;
1174
+ }
1175
+ case "PageUp":
1176
+ event.preventDefault();
1177
+ if (event.shiftKey)
1178
+ this.shiftYear(-1);
1179
+ else
1180
+ this.shiftMonth(-1);
1181
+ break;
1182
+ case "PageDown":
1183
+ event.preventDefault();
1184
+ if (event.shiftKey)
1185
+ this.shiftYear(1);
1186
+ else
1187
+ this.shiftMonth(1);
1188
+ break;
1189
+ case "Enter":
1190
+ case " ":
1191
+ event.preventDefault();
1192
+ this.selectDay(this.cursor());
1193
+ break;
1194
+ default:
1195
+ break;
1196
+ }
1197
+ }
1198
+ // ---------------------------------------------------------------
1199
+ // Dialog keys and the focus trap
1200
+ // ---------------------------------------------------------------
1201
+ /** Everything tabbable inside the dialog, in DOM order. */
1202
+ focusables() {
1203
+ const dialogEl = this.dialogRef()?.nativeElement;
1204
+ if (!dialogEl)
1205
+ return [];
1206
+ return Array.from(dialogEl.querySelectorAll('button:not([disabled]):not([tabindex="-1"]), select:not([disabled])'));
1207
+ }
1208
+ onDialogKeydown(event) {
1209
+ if (event.key === "Escape") {
1210
+ event.preventDefault();
1211
+ // Escape discards: `value` is untouched, so the field still shows
1212
+ // whatever was committed before the dialog opened.
1213
+ this.closeDialog();
1214
+ return;
1215
+ }
1216
+ if (event.key !== "Tab")
1217
+ return;
1218
+ // The trap. `aria-modal="true"` is a promise that focus cannot reach
1219
+ // the page behind the dialog, and it is a promise the browser does
1220
+ // not keep for us: an untrapped aria-modal dialog tells a screen
1221
+ // reader the rest of the page is inert while Tab quietly walks into
1222
+ // it.
1223
+ const all = this.focusables();
1224
+ if (all.length === 0)
1225
+ return;
1226
+ const first = all[0];
1227
+ const last = all[all.length - 1];
1228
+ const active = document.activeElement;
1229
+ const dialogEl = this.dialogRef()?.nativeElement;
1230
+ if (event.shiftKey && (active === first || !dialogEl?.contains(active))) {
1231
+ event.preventDefault();
1232
+ last.focus?.();
1233
+ }
1234
+ else if (!event.shiftKey && active === last) {
1235
+ event.preventDefault();
1236
+ first.focus?.();
1237
+ }
1238
+ }
1239
+ // ---------------------------------------------------------------
1240
+ // Text field
1241
+ // ---------------------------------------------------------------
1242
+ /** Default text parsing, per mode. */
1243
+ parseTypedForMode(text) {
1244
+ if (this.mode() === "time")
1245
+ return parseTimeInput(text);
1246
+ if (this.mode() === "date")
1247
+ return parseDateInput(text, this.locale() || undefined);
1248
+ // datetime: split on the last whitespace run or a literal T, and
1249
+ // require both halves — see `commit` on why half is refused.
1250
+ const m = /^(.*?)[T\s]+([^\sT]+)$/.exec(text.trim());
1251
+ if (!m)
1252
+ return null;
1253
+ const date = parseDateInput(m[1], this.locale() || undefined);
1254
+ const time = parseTimeInput(m[2]);
1255
+ return date && time ? `${date}T${time}` : null;
1256
+ }
1257
+ /** Resolve typed text on blur or Enter. */
1258
+ resolveTyped() {
1259
+ const text = this.typed();
1260
+ if (text === null)
1261
+ return;
1262
+ if (!text.trim()) {
1263
+ this.typed.set(null);
1264
+ this.invalid.set(false);
1265
+ if (this.value() !== "") {
1266
+ this.value.set("");
1267
+ this.change.emit("");
1268
+ }
1269
+ return;
1270
+ }
1271
+ const parser = this.parseInput();
1272
+ const parsed = parser ? parser(text) : this.parseTypedForMode(text);
1273
+ if (!parsed) {
1274
+ this.invalid.set(true);
1275
+ this.invalidInput.emit(text);
1276
+ return;
1277
+ }
1278
+ const { date } = splitValue(parsed, this.mode());
1279
+ if (this.usesDate() && date && this.dayDisabled(date)) {
1280
+ // Parseable but out of bounds. Same outcome as unparseable: the
1281
+ // text stays put and the field is marked invalid, rather than being
1282
+ // silently snapped to some nearby legal date the user never typed.
1283
+ this.invalid.set(true);
1284
+ this.invalidInput.emit(text);
1285
+ return;
1286
+ }
1287
+ this.typed.set(null);
1288
+ this.invalid.set(false);
1289
+ if (parsed !== this.value()) {
1290
+ this.value.set(parsed);
1291
+ this.change.emit(parsed);
1292
+ }
1293
+ }
1294
+ onFieldKeydown(event) {
1295
+ if (event.key === "Enter") {
1296
+ event.preventDefault();
1297
+ this.resolveTyped();
1298
+ }
1299
+ else if (event.key === "ArrowDown" && event.altKey) {
1300
+ // The platform convention for "open the picker" from a field,
1301
+ // matching <input type="date"> in every major browser.
1302
+ event.preventDefault();
1303
+ this.openDialog();
1304
+ }
1305
+ else if (event.key === "Escape" && this.typed() !== null) {
1306
+ // Discard the pending edit and show the committed value again —
1307
+ // the same contract Escape has inside the dialog. Stops
1308
+ // propagating so a surrounding dialog does not also close on
1309
+ // what was, to the user, a text-editing keystroke. When no edit
1310
+ // is pending the key is left alone.
1311
+ event.preventDefault();
1312
+ event.stopPropagation();
1313
+ this.typed.set(null);
1314
+ this.invalid.set(false);
1315
+ }
1316
+ }
1317
+ // ---------------------------------------------------------------
1318
+ // Time selects
1319
+ // ---------------------------------------------------------------
1320
+ setHour(hour) {
1321
+ this.pendingTime.set(formatIsoTime({ hour, minute: this.pendingMinute() }));
1322
+ }
1323
+ setMinute(minute) {
1324
+ this.pendingTime.set(formatIsoTime({ hour: this.pendingHour(), minute }));
1325
+ }
1326
+ /** Cross between AM and PM without changing the minute of the hour. */
1327
+ setMeridiem(pm) {
1328
+ this.setHour((this.pendingHour() % 12) + (pm ? 12 : 0));
1329
+ }
1330
+ // ---------------------------------------------------------------
1331
+ // Shortcuts
1332
+ // ---------------------------------------------------------------
1333
+ applyShortcut(shortcut) {
1334
+ const base = todayIso();
1335
+ let target = shortcut.date ?? base;
1336
+ if (shortcut.days !== undefined)
1337
+ target = addDays(base, shortcut.days);
1338
+ else if (shortcut.months !== undefined) {
1339
+ target = addMonths(base, shortcut.months);
1340
+ }
1341
+ // A shortcut to a blocked date does nothing rather than landing
1342
+ // somewhere near it: "+4 weeks" that quietly means "+27 days" is a
1343
+ // booking error waiting to happen.
1344
+ if (this.dayDisabled(target))
1345
+ return;
1346
+ this.pendingDate.set(target);
1347
+ this.cursor.set(target);
1348
+ const parsed = parseIsoDate(target);
1349
+ if (parsed) {
1350
+ this.viewYear.set(parsed.year);
1351
+ this.viewMonth.set(parsed.month);
1352
+ }
1353
+ this.shortcut.emit({ id: shortcut.id, isoDate: target });
1354
+ if (this.commitOnDay())
1355
+ this.commit();
1356
+ else {
1357
+ // Render-then-focus: the shortcut may have paged to a month whose
1358
+ // cells do not exist until change detection has run. See `cdr`.
1359
+ this.cdr.detectChanges();
1360
+ this.focusCursor();
1361
+ }
1362
+ }
1363
+ // ---------------------------------------------------------------
1364
+ // Dismissal
1365
+ // ---------------------------------------------------------------
1366
+ onDocumentClick(event) {
1367
+ if (!this.open())
1368
+ return;
1369
+ const target = event.target;
1370
+ if (!target)
1371
+ return;
1372
+ // Anything outside the dialog closes it — including the component's
1373
+ // own text field. The dialog says aria-modal="true", and a modal
1374
+ // that stays open while the user edits the field behind it is
1375
+ // telling assistive technology one thing and doing another. The
1376
+ // trigger button is exempt because its own handler already toggles.
1377
+ if (this.dialogRef().nativeElement.contains(target) ||
1378
+ this.buttonRef().nativeElement.contains(target)) {
1379
+ return;
1380
+ }
1381
+ this.closeDialog(false);
1382
+ }
1383
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: DateTimePicker, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1384
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: DateTimePicker, isStandalone: true, selector: "lily-date-time-picker", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: true, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, isDateDisabled: { classPropertyName: "isDateDisabled", publicName: "isDateDisabled", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null }, hour12: { classPropertyName: "hour12", publicName: "hour12", isSignal: true, isRequired: false, transformFunction: null }, showWeekNumbers: { classPropertyName: "showWeekNumbers", publicName: "showWeekNumbers", isSignal: true, isRequired: false, transformFunction: null }, shortcuts: { classPropertyName: "shortcuts", publicName: "shortcuts", isSignal: true, isRequired: false, transformFunction: null }, confirmOnSelect: { classPropertyName: "confirmOnSelect", publicName: "confirmOnSelect", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, timeZone: { classPropertyName: "timeZone", publicName: "timeZone", isSignal: true, isRequired: false, transformFunction: null }, timeZones: { classPropertyName: "timeZones", publicName: "timeZones", isSignal: true, isRequired: false, transformFunction: null }, timeZoneLabels: { classPropertyName: "timeZoneLabels", publicName: "timeZoneLabels", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, describedBy: { classPropertyName: "describedBy", publicName: "describedBy", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", 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 }, formatValue: { classPropertyName: "formatValue", publicName: "formatValue", isSignal: true, isRequired: false, transformFunction: null }, parseInput: { classPropertyName: "parseInput", publicName: "parseInput", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", timeZone: "timeZoneChange", change: "change", shortcut: "shortcut", invalidInput: "invalidInput" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, queries: [{ propertyName: "iconTemplate", first: true, predicate: TemplateRef, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "rootRef", first: true, predicate: ["rootEl"], descendants: true, isSignal: true }, { propertyName: "buttonRef", first: true, predicate: ["buttonEl"], descendants: true, isSignal: true }, { propertyName: "dialogRef", first: true, predicate: ["dialogEl"], descendants: true, isSignal: true }, { propertyName: "gridRef", first: true, predicate: ["gridEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
1385
+ <div
1386
+ #rootEl
1387
+ class="date-time-picker {{ className() }}"
1388
+ [attr.data-mode]="mode()"
1389
+ [attr.data-time-zone]="timeZone() || null"
1390
+ >
1391
+ <input type="hidden" [name]="name()" [value]="value()" />
1392
+ @if (labels().timeZone) {
1393
+ <input
1394
+ type="hidden"
1395
+ [name]="name() + '-time-zone'"
1396
+ [value]="timeZone()"
1397
+ />
1398
+ }
1399
+
1400
+ <div class="date-time-picker-field">
1401
+ <input
1402
+ class="date-time-picker-input"
1403
+ [id]="fieldId()"
1404
+ type="text"
1405
+ autocomplete="off"
1406
+ [value]="display()"
1407
+ [attr.placeholder]="placeholder() || null"
1408
+ [disabled]="disabled()"
1409
+ [readOnly]="readOnly()"
1410
+ [required]="required()"
1411
+ [attr.aria-describedby]="fieldDescribedBy()"
1412
+ [attr.aria-invalid]="invalid() ? 'true' : null"
1413
+ [attr.aria-errormessage]="
1414
+ invalid() && labels().invalid ? statusId : null
1415
+ "
1416
+ (input)="typed.set($any($event.target).value)"
1417
+ (blur)="resolveTyped()"
1418
+ (keydown)="onFieldKeydown($event)"
1419
+ />
1420
+
1421
+ <button
1422
+ #buttonEl
1423
+ type="button"
1424
+ class="date-time-picker-button"
1425
+ [attr.aria-label]="label()"
1426
+ aria-haspopup="dialog"
1427
+ [attr.aria-expanded]="open()"
1428
+ [attr.aria-controls]="dialogId"
1429
+ [disabled]="disabled() || readOnly()"
1430
+ (click)="open() ? closeDialog() : openDialog()"
1431
+ >
1432
+ @if (iconTemplate(); as tpl) {
1433
+ <ng-container
1434
+ [ngTemplateOutlet]="tpl"
1435
+ [ngTemplateOutletContext]="childContext()"
1436
+ />
1437
+ } @else {
1438
+ <span class="date-time-picker-icon" aria-hidden="true">{{
1439
+ glyph
1440
+ }}</span>
1441
+ }
1442
+ </button>
1443
+ </div>
1444
+
1445
+ @if (labels().invalid) {
1446
+ <!-- Present in the DOM before it has content: a live region that
1447
+ appears at the same moment as its message is routinely not
1448
+ announced at all. Empty while the field is valid. -->
1449
+ <span class="date-time-picker-status" [id]="statusId" role="status">{{
1450
+ invalid() ? labels().invalid : ""
1451
+ }}</span>
1452
+ }
1453
+
1454
+ <div
1455
+ #dialogEl
1456
+ class="date-time-picker-dialog"
1457
+ [id]="dialogId"
1458
+ role="dialog"
1459
+ aria-modal="true"
1460
+ [attr.aria-label]="label()"
1461
+ [attr.aria-describedby]="labels().instructions ? instructionsId : null"
1462
+ tabindex="-1"
1463
+ [attr.hidden]="open() ? null : ''"
1464
+ (keydown)="onDialogKeydown($event)"
1465
+ >
1466
+ @if (labels().instructions) {
1467
+ <!-- Spoken once when the dialog takes focus, via the dialog's
1468
+ aria-describedby. Visible by default; a consumer who wants
1469
+ it screen-reader-only hides it with their own CSS. -->
1470
+ <p class="date-time-picker-instructions" [id]="instructionsId">
1471
+ {{ labels().instructions }}
1472
+ </p>
1473
+ }
1474
+ @if (usesDate()) {
1475
+ <div class="date-time-picker-header">
1476
+ <button
1477
+ type="button"
1478
+ class="date-time-picker-previous-year"
1479
+ [attr.aria-label]="labels().previousYear"
1480
+ (click)="shiftYear(-1)"
1481
+ >
1482
+ <span aria-hidden="true">&#171;</span>
1483
+ </button>
1484
+ <button
1485
+ type="button"
1486
+ class="date-time-picker-previous-month"
1487
+ [attr.aria-label]="labels().previousMonth"
1488
+ (click)="shiftMonth(-1)"
1489
+ >
1490
+ <span aria-hidden="true">&#8249;</span>
1491
+ </button>
1492
+ <button
1493
+ type="button"
1494
+ class="date-time-picker-previous-week"
1495
+ [attr.aria-label]="labels().previousWeek"
1496
+ (click)="shiftDays(-7)"
1497
+ >
1498
+ <span aria-hidden="true">&#8249;&#8249;</span>
1499
+ </button>
1500
+ <button
1501
+ type="button"
1502
+ class="date-time-picker-previous-day"
1503
+ [attr.aria-label]="labels().previousDay"
1504
+ (click)="shiftDays(-1)"
1505
+ >
1506
+ <span aria-hidden="true">&#8249;</span>
1507
+ </button>
1508
+
1509
+ <!-- Polite, not assertive: paging months is the visible result
1510
+ of the user's own keypress, so it should reach a screen
1511
+ reader without interrupting anything. -->
1512
+ <span
1513
+ class="date-time-picker-period"
1514
+ [id]="periodId"
1515
+ aria-live="polite"
1516
+ >{{ periodText() }}</span
1517
+ >
1518
+
1519
+ <button
1520
+ type="button"
1521
+ class="date-time-picker-next-day"
1522
+ [attr.aria-label]="labels().nextDay"
1523
+ (click)="shiftDays(1)"
1524
+ >
1525
+ <span aria-hidden="true">&#8250;</span>
1526
+ </button>
1527
+ <button
1528
+ type="button"
1529
+ class="date-time-picker-next-week"
1530
+ [attr.aria-label]="labels().nextWeek"
1531
+ (click)="shiftDays(7)"
1532
+ >
1533
+ <span aria-hidden="true">&#8250;&#8250;</span>
1534
+ </button>
1535
+ <button
1536
+ type="button"
1537
+ class="date-time-picker-next-month"
1538
+ [attr.aria-label]="labels().nextMonth"
1539
+ (click)="shiftMonth(1)"
1540
+ >
1541
+ <span aria-hidden="true">&#8250;</span>
1542
+ </button>
1543
+ <button
1544
+ type="button"
1545
+ class="date-time-picker-next-year"
1546
+ [attr.aria-label]="labels().nextYear"
1547
+ (click)="shiftYear(1)"
1548
+ >
1549
+ <span aria-hidden="true">&#187;</span>
1550
+ </button>
1551
+ </div>
1552
+
1553
+ @if (labels().timeZone) {
1554
+ <!-- Before the grid, so the zone is chosen before the instant.
1555
+ The empty first option is the "no zone" state: the picker
1556
+ never guesses one from the runtime. -->
1557
+ <div class="date-time-picker-time-zone">
1558
+ <label
1559
+ class="date-time-picker-time-zone-label"
1560
+ [for]="timeZoneId"
1561
+ >{{ labels().timeZone }}</label
1562
+ >
1563
+ <!-- A parent [value] binding on <select> races the @for
1564
+ block's own per-option bindings: Angular's update pass
1565
+ can apply the select's value before the dynamically
1566
+ created <option> elements have their own [value]
1567
+ applied, so the browser matches nothing. Binding
1568
+ [selected] on each <option> instead selects correctly
1569
+ regardless of that ordering, because it is evaluated as
1570
+ part of the same per-item update as the option's own
1571
+ value/text. -->
1572
+ <select
1573
+ class="date-time-picker-time-zone-select"
1574
+ [id]="timeZoneId"
1575
+ (change)="onTimeZoneSelect($event)"
1576
+ >
1577
+ <option value="" [selected]="timeZone() === ''"></option>
1578
+ @for (zone of zoneOptions(); track zone) {
1579
+ <option [value]="zone" [selected]="zone === timeZone()">
1580
+ {{ zoneLabel(zone) }}
1581
+ </option>
1582
+ }
1583
+ </select>
1584
+ </div>
1585
+ }
1586
+
1587
+ <!-- The grid owns its own keyboard contract, which is why the
1588
+ handler sits on the table rather than on each of 42 cells. -->
1589
+ <table
1590
+ #gridEl
1591
+ class="date-time-picker-calendar"
1592
+ role="grid"
1593
+ [attr.aria-labelledby]="periodId"
1594
+ (keydown)="onGridKeydown($event)"
1595
+ >
1596
+ <thead>
1597
+ <tr>
1598
+ @if (showWeekNumbers()) {
1599
+ <th
1600
+ class="date-time-picker-week-heading"
1601
+ scope="col"
1602
+ [attr.abbr]="labels().week"
1603
+ >
1604
+ {{ labels().week }}
1605
+ </th>
1606
+ }
1607
+ @for (weekday of weekdays(); track $index) {
1608
+ <!-- abbr carries the full weekday name, so a screen
1609
+ reader announcing the column says "Monday" where the
1610
+ eye reads "Mo". -->
1611
+ <th
1612
+ class="date-time-picker-weekday"
1613
+ scope="col"
1614
+ [attr.abbr]="weekday.long"
1615
+ >
1616
+ {{ weekday.short }}
1617
+ </th>
1618
+ }
1619
+ </tr>
1620
+ </thead>
1621
+ <tbody>
1622
+ @for (week of weeks(); track week[0]) {
1623
+ <tr>
1624
+ @if (showWeekNumbers()) {
1625
+ <th class="date-time-picker-week" scope="row">
1626
+ {{ isoWeekOf(week[0]) }}
1627
+ </th>
1628
+ }
1629
+ @for (isoDate of week; track isoDate) {
1630
+ @let cell = parseCell(isoDate);
1631
+ @let isToday = isoDate === today();
1632
+ @let isSelected = isoDate === pendingDate();
1633
+ @let isDisabled = dayDisabled(isoDate);
1634
+ <td role="gridcell" [attr.aria-selected]="isSelected">
1635
+ <!-- aria-disabled, not the disabled attribute: a
1636
+ vetoed day must stay focusable so the roving
1637
+ cursor can land on it and a screen reader can
1638
+ announce it as unavailable. A disabled button
1639
+ refuses focus, and arrowing across a blocked
1640
+ week goes silent while the visible focus stays
1641
+ behind. Activation is refused in selectDay
1642
+ instead. -->
1643
+ <button
1644
+ type="button"
1645
+ class="date-time-picker-day"
1646
+ [attr.data-date]="isoDate"
1647
+ [attr.data-outside]="cell.outside ? '' : null"
1648
+ [attr.data-today]="isToday ? '' : null"
1649
+ [attr.data-selected]="isSelected ? '' : null"
1650
+ [attr.data-disabled]="isDisabled ? '' : null"
1651
+ [attr.tabindex]="isoDate === cursor() ? 0 : -1"
1652
+ [attr.aria-label]="dayLabel(isoDate)"
1653
+ [attr.aria-current]="isToday ? 'date' : null"
1654
+ [attr.aria-disabled]="isDisabled ? 'true' : null"
1655
+ (click)="selectDay(isoDate)"
1656
+ >
1657
+ {{ cell.day }}
1658
+ </button>
1659
+ </td>
1660
+ }
1661
+ </tr>
1662
+ }
1663
+ </tbody>
1664
+ </table>
1665
+ }
1666
+
1667
+ @if (usesTime()) {
1668
+ <div class="date-time-picker-time">
1669
+ <label class="date-time-picker-time-label" [for]="hourId">
1670
+ {{ labels().hour }}
1671
+ </label>
1672
+ <select
1673
+ class="date-time-picker-hour"
1674
+ [id]="hourId"
1675
+ [value]="pendingHour()"
1676
+ (change)="setHour(+$any($event.target).value)"
1677
+ >
1678
+ @for (option of hourOptions(); track option.value) {
1679
+ <option [value]="option.value">{{ option.label }}</option>
1680
+ }
1681
+ </select>
1682
+
1683
+ <label class="date-time-picker-time-label" [for]="minuteId">
1684
+ {{ labels().minute }}
1685
+ </label>
1686
+ <select
1687
+ class="date-time-picker-minute"
1688
+ [id]="minuteId"
1689
+ [value]="pendingMinute()"
1690
+ (change)="setMinute(+$any($event.target).value)"
1691
+ >
1692
+ @for (minute of minuteOptions(); track minute) {
1693
+ <option [value]="minute">{{ pad2(minute) }}</option>
1694
+ }
1695
+ </select>
1696
+
1697
+ @if (clock12()) {
1698
+ <label class="date-time-picker-time-label" [for]="meridiemId">
1699
+ {{ labels().meridiem }}
1700
+ </label>
1701
+ <select
1702
+ class="date-time-picker-meridiem"
1703
+ [id]="meridiemId"
1704
+ [value]="pendingHour() >= 12 ? 'pm' : 'am'"
1705
+ (change)="setMeridiem($any($event.target).value === 'pm')"
1706
+ >
1707
+ <!-- Stable values, localised labels: the visible text is
1708
+ the locale's own day-period name. -->
1709
+ <option value="am">{{ dayPeriodName(false) }}</option>
1710
+ <option value="pm">{{ dayPeriodName(true) }}</option>
1711
+ </select>
1712
+ }
1713
+ </div>
1714
+ }
1715
+
1716
+ @if (shortcuts().length > 0) {
1717
+ <div class="date-time-picker-shortcuts">
1718
+ @for (shortcut of shortcuts(); track shortcut.id) {
1719
+ <button
1720
+ type="button"
1721
+ class="date-time-picker-shortcut"
1722
+ [attr.data-shortcut-id]="shortcut.id"
1723
+ (click)="applyShortcut(shortcut)"
1724
+ >
1725
+ {{ shortcut.label }}
1726
+ </button>
1727
+ }
1728
+ </div>
1729
+ }
1730
+
1731
+ <div class="date-time-picker-footer">
1732
+ @if (labels().clear) {
1733
+ <button
1734
+ type="button"
1735
+ class="date-time-picker-clear"
1736
+ (click)="clear()"
1737
+ >
1738
+ {{ labels().clear }}
1739
+ </button>
1740
+ }
1741
+ <button
1742
+ type="button"
1743
+ class="date-time-picker-cancel"
1744
+ (click)="closeDialog()"
1745
+ >
1746
+ {{ labels().cancel }}
1747
+ </button>
1748
+ <button
1749
+ type="button"
1750
+ class="date-time-picker-confirm"
1751
+ (click)="commit()"
1752
+ >
1753
+ {{ labels().confirm }}
1754
+ </button>
1755
+ </div>
1756
+ </div>
1757
+ </div>
1758
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1759
+ }
1760
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: DateTimePicker, decorators: [{
1761
+ type: Component,
1762
+ args: [{
1763
+ selector: "lily-date-time-picker",
1764
+ standalone: true,
1765
+ imports: [NgTemplateOutlet],
1766
+ changeDetection: ChangeDetectionStrategy.OnPush,
1767
+ host: {
1768
+ "(document:click)": "onDocumentClick($event)",
1769
+ },
1770
+ template: `
1771
+ <div
1772
+ #rootEl
1773
+ class="date-time-picker {{ className() }}"
1774
+ [attr.data-mode]="mode()"
1775
+ [attr.data-time-zone]="timeZone() || null"
1776
+ >
1777
+ <input type="hidden" [name]="name()" [value]="value()" />
1778
+ @if (labels().timeZone) {
1779
+ <input
1780
+ type="hidden"
1781
+ [name]="name() + '-time-zone'"
1782
+ [value]="timeZone()"
1783
+ />
1784
+ }
1785
+
1786
+ <div class="date-time-picker-field">
1787
+ <input
1788
+ class="date-time-picker-input"
1789
+ [id]="fieldId()"
1790
+ type="text"
1791
+ autocomplete="off"
1792
+ [value]="display()"
1793
+ [attr.placeholder]="placeholder() || null"
1794
+ [disabled]="disabled()"
1795
+ [readOnly]="readOnly()"
1796
+ [required]="required()"
1797
+ [attr.aria-describedby]="fieldDescribedBy()"
1798
+ [attr.aria-invalid]="invalid() ? 'true' : null"
1799
+ [attr.aria-errormessage]="
1800
+ invalid() && labels().invalid ? statusId : null
1801
+ "
1802
+ (input)="typed.set($any($event.target).value)"
1803
+ (blur)="resolveTyped()"
1804
+ (keydown)="onFieldKeydown($event)"
1805
+ />
1806
+
1807
+ <button
1808
+ #buttonEl
1809
+ type="button"
1810
+ class="date-time-picker-button"
1811
+ [attr.aria-label]="label()"
1812
+ aria-haspopup="dialog"
1813
+ [attr.aria-expanded]="open()"
1814
+ [attr.aria-controls]="dialogId"
1815
+ [disabled]="disabled() || readOnly()"
1816
+ (click)="open() ? closeDialog() : openDialog()"
1817
+ >
1818
+ @if (iconTemplate(); as tpl) {
1819
+ <ng-container
1820
+ [ngTemplateOutlet]="tpl"
1821
+ [ngTemplateOutletContext]="childContext()"
1822
+ />
1823
+ } @else {
1824
+ <span class="date-time-picker-icon" aria-hidden="true">{{
1825
+ glyph
1826
+ }}</span>
1827
+ }
1828
+ </button>
1829
+ </div>
1830
+
1831
+ @if (labels().invalid) {
1832
+ <!-- Present in the DOM before it has content: a live region that
1833
+ appears at the same moment as its message is routinely not
1834
+ announced at all. Empty while the field is valid. -->
1835
+ <span class="date-time-picker-status" [id]="statusId" role="status">{{
1836
+ invalid() ? labels().invalid : ""
1837
+ }}</span>
1838
+ }
1839
+
1840
+ <div
1841
+ #dialogEl
1842
+ class="date-time-picker-dialog"
1843
+ [id]="dialogId"
1844
+ role="dialog"
1845
+ aria-modal="true"
1846
+ [attr.aria-label]="label()"
1847
+ [attr.aria-describedby]="labels().instructions ? instructionsId : null"
1848
+ tabindex="-1"
1849
+ [attr.hidden]="open() ? null : ''"
1850
+ (keydown)="onDialogKeydown($event)"
1851
+ >
1852
+ @if (labels().instructions) {
1853
+ <!-- Spoken once when the dialog takes focus, via the dialog's
1854
+ aria-describedby. Visible by default; a consumer who wants
1855
+ it screen-reader-only hides it with their own CSS. -->
1856
+ <p class="date-time-picker-instructions" [id]="instructionsId">
1857
+ {{ labels().instructions }}
1858
+ </p>
1859
+ }
1860
+ @if (usesDate()) {
1861
+ <div class="date-time-picker-header">
1862
+ <button
1863
+ type="button"
1864
+ class="date-time-picker-previous-year"
1865
+ [attr.aria-label]="labels().previousYear"
1866
+ (click)="shiftYear(-1)"
1867
+ >
1868
+ <span aria-hidden="true">&#171;</span>
1869
+ </button>
1870
+ <button
1871
+ type="button"
1872
+ class="date-time-picker-previous-month"
1873
+ [attr.aria-label]="labels().previousMonth"
1874
+ (click)="shiftMonth(-1)"
1875
+ >
1876
+ <span aria-hidden="true">&#8249;</span>
1877
+ </button>
1878
+ <button
1879
+ type="button"
1880
+ class="date-time-picker-previous-week"
1881
+ [attr.aria-label]="labels().previousWeek"
1882
+ (click)="shiftDays(-7)"
1883
+ >
1884
+ <span aria-hidden="true">&#8249;&#8249;</span>
1885
+ </button>
1886
+ <button
1887
+ type="button"
1888
+ class="date-time-picker-previous-day"
1889
+ [attr.aria-label]="labels().previousDay"
1890
+ (click)="shiftDays(-1)"
1891
+ >
1892
+ <span aria-hidden="true">&#8249;</span>
1893
+ </button>
1894
+
1895
+ <!-- Polite, not assertive: paging months is the visible result
1896
+ of the user's own keypress, so it should reach a screen
1897
+ reader without interrupting anything. -->
1898
+ <span
1899
+ class="date-time-picker-period"
1900
+ [id]="periodId"
1901
+ aria-live="polite"
1902
+ >{{ periodText() }}</span
1903
+ >
1904
+
1905
+ <button
1906
+ type="button"
1907
+ class="date-time-picker-next-day"
1908
+ [attr.aria-label]="labels().nextDay"
1909
+ (click)="shiftDays(1)"
1910
+ >
1911
+ <span aria-hidden="true">&#8250;</span>
1912
+ </button>
1913
+ <button
1914
+ type="button"
1915
+ class="date-time-picker-next-week"
1916
+ [attr.aria-label]="labels().nextWeek"
1917
+ (click)="shiftDays(7)"
1918
+ >
1919
+ <span aria-hidden="true">&#8250;&#8250;</span>
1920
+ </button>
1921
+ <button
1922
+ type="button"
1923
+ class="date-time-picker-next-month"
1924
+ [attr.aria-label]="labels().nextMonth"
1925
+ (click)="shiftMonth(1)"
1926
+ >
1927
+ <span aria-hidden="true">&#8250;</span>
1928
+ </button>
1929
+ <button
1930
+ type="button"
1931
+ class="date-time-picker-next-year"
1932
+ [attr.aria-label]="labels().nextYear"
1933
+ (click)="shiftYear(1)"
1934
+ >
1935
+ <span aria-hidden="true">&#187;</span>
1936
+ </button>
1937
+ </div>
1938
+
1939
+ @if (labels().timeZone) {
1940
+ <!-- Before the grid, so the zone is chosen before the instant.
1941
+ The empty first option is the "no zone" state: the picker
1942
+ never guesses one from the runtime. -->
1943
+ <div class="date-time-picker-time-zone">
1944
+ <label
1945
+ class="date-time-picker-time-zone-label"
1946
+ [for]="timeZoneId"
1947
+ >{{ labels().timeZone }}</label
1948
+ >
1949
+ <!-- A parent [value] binding on <select> races the @for
1950
+ block's own per-option bindings: Angular's update pass
1951
+ can apply the select's value before the dynamically
1952
+ created <option> elements have their own [value]
1953
+ applied, so the browser matches nothing. Binding
1954
+ [selected] on each <option> instead selects correctly
1955
+ regardless of that ordering, because it is evaluated as
1956
+ part of the same per-item update as the option's own
1957
+ value/text. -->
1958
+ <select
1959
+ class="date-time-picker-time-zone-select"
1960
+ [id]="timeZoneId"
1961
+ (change)="onTimeZoneSelect($event)"
1962
+ >
1963
+ <option value="" [selected]="timeZone() === ''"></option>
1964
+ @for (zone of zoneOptions(); track zone) {
1965
+ <option [value]="zone" [selected]="zone === timeZone()">
1966
+ {{ zoneLabel(zone) }}
1967
+ </option>
1968
+ }
1969
+ </select>
1970
+ </div>
1971
+ }
1972
+
1973
+ <!-- The grid owns its own keyboard contract, which is why the
1974
+ handler sits on the table rather than on each of 42 cells. -->
1975
+ <table
1976
+ #gridEl
1977
+ class="date-time-picker-calendar"
1978
+ role="grid"
1979
+ [attr.aria-labelledby]="periodId"
1980
+ (keydown)="onGridKeydown($event)"
1981
+ >
1982
+ <thead>
1983
+ <tr>
1984
+ @if (showWeekNumbers()) {
1985
+ <th
1986
+ class="date-time-picker-week-heading"
1987
+ scope="col"
1988
+ [attr.abbr]="labels().week"
1989
+ >
1990
+ {{ labels().week }}
1991
+ </th>
1992
+ }
1993
+ @for (weekday of weekdays(); track $index) {
1994
+ <!-- abbr carries the full weekday name, so a screen
1995
+ reader announcing the column says "Monday" where the
1996
+ eye reads "Mo". -->
1997
+ <th
1998
+ class="date-time-picker-weekday"
1999
+ scope="col"
2000
+ [attr.abbr]="weekday.long"
2001
+ >
2002
+ {{ weekday.short }}
2003
+ </th>
2004
+ }
2005
+ </tr>
2006
+ </thead>
2007
+ <tbody>
2008
+ @for (week of weeks(); track week[0]) {
2009
+ <tr>
2010
+ @if (showWeekNumbers()) {
2011
+ <th class="date-time-picker-week" scope="row">
2012
+ {{ isoWeekOf(week[0]) }}
2013
+ </th>
2014
+ }
2015
+ @for (isoDate of week; track isoDate) {
2016
+ @let cell = parseCell(isoDate);
2017
+ @let isToday = isoDate === today();
2018
+ @let isSelected = isoDate === pendingDate();
2019
+ @let isDisabled = dayDisabled(isoDate);
2020
+ <td role="gridcell" [attr.aria-selected]="isSelected">
2021
+ <!-- aria-disabled, not the disabled attribute: a
2022
+ vetoed day must stay focusable so the roving
2023
+ cursor can land on it and a screen reader can
2024
+ announce it as unavailable. A disabled button
2025
+ refuses focus, and arrowing across a blocked
2026
+ week goes silent while the visible focus stays
2027
+ behind. Activation is refused in selectDay
2028
+ instead. -->
2029
+ <button
2030
+ type="button"
2031
+ class="date-time-picker-day"
2032
+ [attr.data-date]="isoDate"
2033
+ [attr.data-outside]="cell.outside ? '' : null"
2034
+ [attr.data-today]="isToday ? '' : null"
2035
+ [attr.data-selected]="isSelected ? '' : null"
2036
+ [attr.data-disabled]="isDisabled ? '' : null"
2037
+ [attr.tabindex]="isoDate === cursor() ? 0 : -1"
2038
+ [attr.aria-label]="dayLabel(isoDate)"
2039
+ [attr.aria-current]="isToday ? 'date' : null"
2040
+ [attr.aria-disabled]="isDisabled ? 'true' : null"
2041
+ (click)="selectDay(isoDate)"
2042
+ >
2043
+ {{ cell.day }}
2044
+ </button>
2045
+ </td>
2046
+ }
2047
+ </tr>
2048
+ }
2049
+ </tbody>
2050
+ </table>
2051
+ }
2052
+
2053
+ @if (usesTime()) {
2054
+ <div class="date-time-picker-time">
2055
+ <label class="date-time-picker-time-label" [for]="hourId">
2056
+ {{ labels().hour }}
2057
+ </label>
2058
+ <select
2059
+ class="date-time-picker-hour"
2060
+ [id]="hourId"
2061
+ [value]="pendingHour()"
2062
+ (change)="setHour(+$any($event.target).value)"
2063
+ >
2064
+ @for (option of hourOptions(); track option.value) {
2065
+ <option [value]="option.value">{{ option.label }}</option>
2066
+ }
2067
+ </select>
2068
+
2069
+ <label class="date-time-picker-time-label" [for]="minuteId">
2070
+ {{ labels().minute }}
2071
+ </label>
2072
+ <select
2073
+ class="date-time-picker-minute"
2074
+ [id]="minuteId"
2075
+ [value]="pendingMinute()"
2076
+ (change)="setMinute(+$any($event.target).value)"
2077
+ >
2078
+ @for (minute of minuteOptions(); track minute) {
2079
+ <option [value]="minute">{{ pad2(minute) }}</option>
2080
+ }
2081
+ </select>
2082
+
2083
+ @if (clock12()) {
2084
+ <label class="date-time-picker-time-label" [for]="meridiemId">
2085
+ {{ labels().meridiem }}
2086
+ </label>
2087
+ <select
2088
+ class="date-time-picker-meridiem"
2089
+ [id]="meridiemId"
2090
+ [value]="pendingHour() >= 12 ? 'pm' : 'am'"
2091
+ (change)="setMeridiem($any($event.target).value === 'pm')"
2092
+ >
2093
+ <!-- Stable values, localised labels: the visible text is
2094
+ the locale's own day-period name. -->
2095
+ <option value="am">{{ dayPeriodName(false) }}</option>
2096
+ <option value="pm">{{ dayPeriodName(true) }}</option>
2097
+ </select>
2098
+ }
2099
+ </div>
2100
+ }
2101
+
2102
+ @if (shortcuts().length > 0) {
2103
+ <div class="date-time-picker-shortcuts">
2104
+ @for (shortcut of shortcuts(); track shortcut.id) {
2105
+ <button
2106
+ type="button"
2107
+ class="date-time-picker-shortcut"
2108
+ [attr.data-shortcut-id]="shortcut.id"
2109
+ (click)="applyShortcut(shortcut)"
2110
+ >
2111
+ {{ shortcut.label }}
2112
+ </button>
2113
+ }
2114
+ </div>
2115
+ }
2116
+
2117
+ <div class="date-time-picker-footer">
2118
+ @if (labels().clear) {
2119
+ <button
2120
+ type="button"
2121
+ class="date-time-picker-clear"
2122
+ (click)="clear()"
2123
+ >
2124
+ {{ labels().clear }}
2125
+ </button>
2126
+ }
2127
+ <button
2128
+ type="button"
2129
+ class="date-time-picker-cancel"
2130
+ (click)="closeDialog()"
2131
+ >
2132
+ {{ labels().cancel }}
2133
+ </button>
2134
+ <button
2135
+ type="button"
2136
+ class="date-time-picker-confirm"
2137
+ (click)="commit()"
2138
+ >
2139
+ {{ labels().confirm }}
2140
+ </button>
2141
+ </div>
2142
+ </div>
2143
+ </div>
2144
+ `,
2145
+ }]
2146
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: true }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], isDateDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "isDateDisabled", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], hour12: [{ type: i0.Input, args: [{ isSignal: true, alias: "hour12", required: false }] }], showWeekNumbers: [{ type: i0.Input, args: [{ isSignal: true, alias: "showWeekNumbers", required: false }] }], shortcuts: [{ type: i0.Input, args: [{ isSignal: true, alias: "shortcuts", required: false }] }], confirmOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "confirmOnSelect", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], timeZone: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeZone", required: false }] }, { type: i0.Output, args: ["timeZoneChange"] }], timeZones: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeZones", required: false }] }], timeZoneLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeZoneLabels", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], describedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "describedBy", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readOnly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], formatValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatValue", required: false }] }], parseInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "parseInput", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], shortcut: [{ type: i0.Output, args: ["shortcut"] }], invalidInput: [{ type: i0.Output, args: ["invalidInput"] }], iconTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }], rootRef: [{ type: i0.ViewChild, args: ["rootEl", { isSignal: true }] }], buttonRef: [{ type: i0.ViewChild, args: ["buttonEl", { isSignal: true }] }], dialogRef: [{ type: i0.ViewChild, args: ["dialogEl", { isSignal: true }] }], gridRef: [{ type: i0.ViewChild, args: ["gridEl", { isSignal: true }] }] } });
2147
+
2148
+ /**
2149
+ * Generated bundle index. Do not edit.
2150
+ */
2151
+
2152
+ export { CALENDAR, DateTimePicker, DateTimePickerIcon, addDays, addMonths, daysInMonth, firstDayOfWeekFor, formatIsoDate, formatIsoTime, fromEpochDay, isoWeek, joinValue, monthMatrix, monthNames, nextDateTimePickerId, numericFieldOrder, pad, parseDateInput, parseIsoDate, parseIsoTime, parseTimeInput, splitValue, toEpochDay, weekdayOf, withinRange };
2153
+ //# sourceMappingURL=lilydesignsystem-angular-date-time-picker.mjs.map