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