@lilydesignsystem/angular-date-time-picker 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1392 +1,1391 @@
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, DestroyRef, Directive, TemplateRef, afterNextRender, computed, contentChild, effect, inject, input, model, output, signal, viewChild } from "@angular/core";
4
+ import { IconButton } from "@lilydesignsystem/angular-headless";
5
+ const DAY_MS = 864e5;
6
+ const toMs = (iso) => Date.parse(`${iso}T00:00:00Z`);
7
+ const toIso = (ms) => new Date(ms).toISOString().slice(0, 10);
8
+ const shift = (iso, days) => toIso(toMs(iso) + days * DAY_MS);
9
+ const formatters = /* @__PURE__ */ new Map();
10
+ function dayFormatter(calendar) {
11
+ let formatter = formatters.get(calendar);
12
+ if (!formatter) {
13
+ formatter = new Intl.DateTimeFormat(`en-u-ca-${calendar}-nu-latn`, {
14
+ day: "numeric",
15
+ timeZone: "UTC"
16
+ });
17
+ formatters.set(calendar, formatter);
18
+ }
19
+ return formatter;
20
+ }
21
+ function calendarDay(iso, calendar = "gregory") {
22
+ const part = dayFormatter(calendar).formatToParts(new Date(toMs(iso))).find((p) => p.type === "day");
23
+ return Number(part?.value);
24
+ }
25
+ function calendarMonthStart(iso, calendar = "gregory") {
26
+ return shift(iso, 1 - calendarDay(iso, calendar));
27
+ }
28
+ function calendarMonthLength(start, calendar = "gregory") {
29
+ for (let length = 1; length < 40; length++) if (calendarDay(shift(start, length), calendar) === 1) return length;
30
+ throw new RangeError(`calendar-system: no month end after ${start} in ${calendar}`);
31
+ }
32
+ function calendarAddMonths(start, delta, calendar = "gregory") {
33
+ let month = start;
34
+ for (let i = 0; i < delta; i++) month = shift(month, calendarMonthLength(month, calendar));
35
+ for (let i = 0; i > delta; i--) month = calendarMonthStart(shift(month, -1), calendar);
36
+ return month;
37
+ }
38
+ function calendarMonthMatrix(start, firstDayOfWeek, calendar = "gregory") {
39
+ const weekday = new Date(toMs(start)).getUTCDay();
40
+ let date = shift(start, -((weekday - firstDayOfWeek + 7) % 7));
41
+ const rows = [];
42
+ for (let row = 0; row < 6; row++) {
43
+ const days = [];
44
+ for (let column = 0; column < 7; column++) {
45
+ days.push(date);
46
+ date = shift(date, 1);
47
+ }
48
+ rows.push(days);
49
+ }
50
+ return rows;
51
+ }
52
+ function calendarLocale(locale, calendar = "gregory") {
53
+ if (!locale) return locale;
54
+ try {
55
+ return new Intl.Locale(locale, { calendar }).toString();
56
+ } catch {
57
+ return locale;
58
+ }
59
+ }
60
+ function isRtl(element) {
61
+ if (!element) return false;
62
+ const attr = element.closest("[dir]")?.getAttribute("dir")?.toLowerCase();
63
+ if (attr === "rtl") return true;
64
+ if (attr === "ltr") return false;
65
+ if (typeof getComputedStyle === "function") return getComputedStyle(element).direction === "rtl";
66
+ return false;
67
+ }
68
+ function logicalKey(key, rtl) {
69
+ if (!rtl) return key;
70
+ if (key === "ArrowLeft") return "ArrowRight";
71
+ if (key === "ArrowRight") return "ArrowLeft";
72
+ return key;
73
+ }
18
74
  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
75
  function pad(n, width = 2) {
28
- return String(Math.abs(n)).padStart(width, "0");
76
+ return String(Math.abs(n)).padStart(width, "0");
29
77
  }
30
- /** Days in a month. `month` is 1-12. */
31
78
  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();
79
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
34
80
  }
35
- /** `{2026, 3, 1}` → `"2026-03-01"`. */
36
81
  function formatIsoDate(date) {
37
- return `${pad(date.year, 4)}-${pad(date.month)}-${pad(date.day)}`;
82
+ return `${pad(date.year, 4)}-${pad(date.month)}-${pad(date.day)}`;
38
83
  }
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
84
  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 };
85
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text.trim());
86
+ if (!m) return null;
87
+ const year = Number(m[1]);
88
+ const month = Number(m[2]);
89
+ const day = Number(m[3]);
90
+ if (month < 1 || month > 12) return null;
91
+ if (day < 1 || day > daysInMonth(year, month)) return null;
92
+ return {
93
+ year,
94
+ month,
95
+ day
96
+ };
58
97
  }
59
- /** Days since the Unix epoch. The unit all date arithmetic goes through. */
60
98
  function toEpochDay(date) {
61
- return Date.UTC(date.year, date.month - 1, date.day) / 86400000;
99
+ return Date.UTC(date.year, date.month - 1, date.day) / 864e5;
62
100
  }
63
- /** Inverse of `toEpochDay`. */
64
101
  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
- };
102
+ const d = /* @__PURE__ */ new Date(epochDay * 864e5);
103
+ return {
104
+ year: d.getUTCFullYear(),
105
+ month: d.getUTCMonth() + 1,
106
+ day: d.getUTCDate()
107
+ };
71
108
  }
72
- /** Shift an ISO date by whole days. */
73
109
  function addDays(isoDate, days) {
74
- const date = parseIsoDate(isoDate);
75
- if (!date)
76
- return isoDate;
77
- return formatIsoDate(fromEpochDay(toEpochDay(date) + days));
110
+ const date = parseIsoDate(isoDate);
111
+ if (!date) return isoDate;
112
+ return formatIsoDate(fromEpochDay(toEpochDay(date) + days));
78
113
  }
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
114
  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
- });
115
+ const date = parseIsoDate(isoDate);
116
+ if (!date) return isoDate;
117
+ const total = date.year * 12 + (date.month - 1) + months;
118
+ const year = Math.floor(total / 12);
119
+ const month = (total % 12 + 12) % 12 + 1;
120
+ return formatIsoDate({
121
+ year,
122
+ month,
123
+ day: Math.min(date.day, daysInMonth(year, month))
124
+ });
98
125
  }
99
- /** Day of week: 0 = Sunday … 6 = Saturday. */
100
126
  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();
127
+ const date = parseIsoDate(isoDate);
128
+ if (!date) return 0;
129
+ return new Date(Date.UTC(date.year, date.month - 1, date.day)).getUTCDay();
105
130
  }
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
131
  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;
132
+ if (!parseIsoDate(isoDate)) return 0;
133
+ const parsed = parseIsoDate(addDays(isoDate, 3 - (weekdayOf(isoDate) + 6) % 7));
134
+ if (!parsed) return 0;
135
+ const jan1 = parseIsoDate(formatIsoDate({
136
+ year: parsed.year,
137
+ month: 1,
138
+ day: 1
139
+ }));
140
+ if (!jan1) return 0;
141
+ return Math.floor((toEpochDay(parsed) - toEpochDay(jan1)) / 7) + 1;
126
142
  }
127
- /** `"09:30"` → `{9, 30}`, or null. */
128
143
  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 };
144
+ const m = /^(\d{2}):(\d{2})$/.exec(text.trim());
145
+ if (!m) return null;
146
+ const hour = Number(m[1]);
147
+ const minute = Number(m[2]);
148
+ if (hour > 23 || minute > 59) return null;
149
+ return {
150
+ hour,
151
+ minute
152
+ };
137
153
  }
138
- /** `{9, 30}` → `"09:30"`. */
139
154
  function formatIsoTime(time) {
140
- return `${pad(time.hour)}:${pad(time.minute)}`;
155
+ return `${pad(time.hour)}:${pad(time.minute)}`;
141
156
  }
142
- /** Pull the date and time halves out of a mode-appropriate ISO value. */
143
157
  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
- };
158
+ if (!value) return {
159
+ date: "",
160
+ time: ""
161
+ };
162
+ if (mode === "time") return {
163
+ date: "",
164
+ time: parseIsoTime(value) ? value : ""
165
+ };
166
+ const [datePart = "", timePart = ""] = value.split("T");
167
+ return {
168
+ date: parseIsoDate(datePart) ? datePart : "",
169
+ time: mode === "datetime" && parseIsoTime(timePart) ? timePart : ""
170
+ };
154
171
  }
155
- /** Recombine the halves. Returns "" when the value is incomplete. */
156
172
  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}` : "";
173
+ if (mode === "date") return date;
174
+ if (mode === "time") return time;
175
+ return date && time ? `${date}T${time}` : "";
162
176
  }
163
- /** Is `isoDate` inside the inclusive [min, max] window? Empty bounds pass. */
164
177
  function withinRange(isoDate, min, max) {
165
- if (min && isoDate < min)
166
- return false;
167
- if (max && isoDate > max)
168
- return false;
169
- return true;
178
+ if (min && isoDate < min) return false;
179
+ if (max && isoDate > max) return false;
180
+ return true;
170
181
  }
171
- /** Uppercase region subtag of a BCP 47 tag, or "". */
172
182
  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 "";
183
+ if (!locale) return "";
184
+ for (const part of locale.split(/[-_]/).slice(1)) if (/^[A-Za-z]{2}$/.test(part)) return part.toUpperCase();
185
+ return "";
180
186
  }
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",
187
+ const SUNDAY_FIRST_REGIONS = /* @__PURE__ */ new Set([
188
+ "AR",
189
+ "BR",
190
+ "CA",
191
+ "CL",
192
+ "CO",
193
+ "DO",
194
+ "GT",
195
+ "HK",
196
+ "IL",
197
+ "IN",
198
+ "JP",
199
+ "KR",
200
+ "MO",
201
+ "MX",
202
+ "PE",
203
+ "PH",
204
+ "PK",
205
+ "TH",
206
+ "TW",
207
+ "US",
208
+ "VE",
209
+ "ZA"
184
210
  ]);
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",
211
+ const SATURDAY_FIRST_REGIONS = /* @__PURE__ */ new Set([
212
+ "AE",
213
+ "AF",
214
+ "BH",
215
+ "DJ",
216
+ "DZ",
217
+ "EG",
218
+ "IQ",
219
+ "IR",
220
+ "JO",
221
+ "KW",
222
+ "LY",
223
+ "OM",
224
+ "QA",
225
+ "SA",
226
+ "SD",
227
+ "SY",
228
+ "YE"
188
229
  ]);
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
230
  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;
231
+ if (locale) try {
232
+ const loc = new Intl.Locale(locale);
233
+ const info = loc.getWeekInfo?.() ?? loc.weekInfo;
234
+ if (info && typeof info.firstDay === "number") return info.firstDay % 7;
235
+ } catch {}
236
+ const region = regionOf(locale);
237
+ if (SUNDAY_FIRST_REGIONS.has(region)) return 0;
238
+ if (SATURDAY_FIRST_REGIONS.has(region)) return 6;
239
+ return 1;
219
240
  }
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
241
  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;
242
+ const first = formatIsoDate({
243
+ year,
244
+ month,
245
+ day: 1
246
+ });
247
+ const start = addDays(first, -((weekdayOf(first) - firstDayOfWeek + 7) % 7));
248
+ const weeks = [];
249
+ for (let row = 0; row < 6; row++) {
250
+ const week = [];
251
+ for (let col = 0; col < 7; col++) week.push(addDays(start, row * 7 + col));
252
+ weeks.push(week);
253
+ }
254
+ return weeks;
242
255
  }
243
- /** Long and short month names for a locale, index 0 = January. */
244
256
  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") };
257
+ const build = (month) => {
258
+ try {
259
+ const fmt = new Intl.DateTimeFormat(gregorian(locale), {
260
+ month,
261
+ timeZone: "UTC"
262
+ });
263
+ return Array.from({ length: 12 }, (_, i) => fmt.format(new Date(Date.UTC(2021, i, 15))));
264
+ } catch {
265
+ return [];
266
+ }
267
+ };
268
+ return {
269
+ long: build("long"),
270
+ short: build("short")
271
+ };
258
272
  }
259
- /** Match a token against a locale's month names. Returns 1-12, or 0. */
260
273
  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;
274
+ const norm = (s) => s.toLocaleLowerCase().replace(/\.$/, "").normalize("NFKD");
275
+ const t = norm(token);
276
+ if (!t || /^\d+$/.test(t)) return 0;
277
+ for (let i = 0; i < 12; i++) {
278
+ if (norm(names.long[i] ?? "") === t) return i + 1;
279
+ if (norm(names.short[i] ?? "") === t) return i + 1;
280
+ }
281
+ if (t.length >= 3) for (let i = 0; i < 12; i++) {
282
+ const long = norm(names.long[i] ?? "");
283
+ if (long && long.startsWith(t)) return i + 1;
284
+ }
285
+ return 0;
282
286
  }
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
287
  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"];
288
+ try {
289
+ const order = new Intl.DateTimeFormat(gregorian(locale), {
290
+ day: "2-digit",
291
+ month: "2-digit",
292
+ year: "numeric",
293
+ timeZone: "UTC"
294
+ }).formatToParts(new Date(Date.UTC(2021, 4, 6))).map((p) => p.type).filter((t) => t === "day" || t === "month" || t === "year");
295
+ if (order.length === 3) return order;
296
+ } catch {}
297
+ return [
298
+ "day",
299
+ "month",
300
+ "year"
301
+ ];
305
302
  }
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
303
  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 });
304
+ const trimmed = normalizeDigits(text).trim();
305
+ if (!trimmed) return null;
306
+ const iso = parseIsoDate(trimmed);
307
+ if (iso) return formatIsoDate(iso);
308
+ const parts = trimmed.split(/[\s./-]+/).filter(Boolean);
309
+ if (parts.length !== 3) return null;
310
+ const names = monthNames(locale);
311
+ let month = 0;
312
+ let monthIndex = -1;
313
+ for (let i = 0; i < parts.length; i++) {
314
+ const found = matchMonthName(parts[i], names);
315
+ if (found) {
316
+ month = found;
317
+ monthIndex = i;
318
+ break;
319
+ }
320
+ }
321
+ let day = 0;
322
+ let year = 0;
323
+ if (monthIndex >= 0) {
324
+ const rest = parts.filter((_, i) => i !== monthIndex).map((p) => Number(p));
325
+ if (rest.some((n) => Number.isNaN(n))) return null;
326
+ [day, year] = rest[0] > rest[1] ? [rest[1], rest[0]] : rest;
327
+ } else {
328
+ const nums = parts.map((p) => Number(p));
329
+ if (nums.some((n) => Number.isNaN(n))) return null;
330
+ const order = numericFieldOrder(locale);
331
+ year = nums[order.indexOf("year")];
332
+ day = nums[order.indexOf("day")];
333
+ month = nums[order.indexOf("month")];
334
+ }
335
+ if (year < 100) year += year < 70 ? 2e3 : 1900;
336
+ if (month < 1 || month > 12) return null;
337
+ if (day < 1 || day > daysInMonth(year, month)) return null;
338
+ return formatIsoDate({
339
+ year,
340
+ month,
341
+ day
342
+ });
343
+ }
344
+ function gregorian(locale) {
345
+ if (!locale) return locale;
346
+ try {
347
+ return new Intl.Locale(locale, { calendar: "gregory" }).toString();
348
+ } catch {
349
+ return locale;
350
+ }
351
+ }
352
+ const NATIVE_DIGITS = /* @__PURE__ */ new Map();
353
+ function nativeDigits() {
354
+ if (NATIVE_DIGITS.size > 0) return NATIVE_DIGITS;
355
+ for (const system of [
356
+ "adlm",
357
+ "ahom",
358
+ "arab",
359
+ "arabext",
360
+ "bali",
361
+ "beng",
362
+ "bhks",
363
+ "brah",
364
+ "cakm",
365
+ "cham",
366
+ "deva",
367
+ "fullwide",
368
+ "gong",
369
+ "gonm",
370
+ "gujr",
371
+ "guru",
372
+ "hanidec",
373
+ "hmng",
374
+ "hmnp",
375
+ "java",
376
+ "kali",
377
+ "kawi",
378
+ "khmr",
379
+ "knda",
380
+ "lana",
381
+ "lanatham",
382
+ "laoo",
383
+ "lepc",
384
+ "limb",
385
+ "mathbold",
386
+ "mathdbl",
387
+ "mathmono",
388
+ "mathsanb",
389
+ "mathsans",
390
+ "mlym",
391
+ "modi",
392
+ "mong",
393
+ "mroo",
394
+ "mtei",
395
+ "mymr",
396
+ "mymrshan",
397
+ "mymrtlng",
398
+ "nagm",
399
+ "newa",
400
+ "nkoo",
401
+ "olck",
402
+ "orya",
403
+ "osma",
404
+ "rohg",
405
+ "saur",
406
+ "segment",
407
+ "shrd",
408
+ "sind",
409
+ "sinh",
410
+ "sora",
411
+ "sund",
412
+ "takr",
413
+ "talu",
414
+ "tamldec",
415
+ "telu",
416
+ "thai",
417
+ "tibt",
418
+ "tirh",
419
+ "tnsa",
420
+ "vaii",
421
+ "wara",
422
+ "wcho"
423
+ ]) try {
424
+ const format = new Intl.NumberFormat(`en-u-nu-${system}`, { useGrouping: false });
425
+ for (let digit = 0; digit < 10; digit++) NATIVE_DIGITS.set(format.format(digit), String(digit));
426
+ } catch {}
427
+ return NATIVE_DIGITS;
370
428
  }
371
- /** Accepts `9:30`, `09:30`, `0930`, `9.30`, and a trailing am/pm. */
372
- 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 });
429
+ function normalizeDigits(text) {
430
+ const map = nativeDigits();
431
+ return text.replace(/\p{Nd}/gu, (d) => map.get(d) ?? d);
432
+ }
433
+ function dayPeriodWords(locale) {
434
+ try {
435
+ const word = (hour) => new Intl.DateTimeFormat(locale, {
436
+ hour: "numeric",
437
+ hourCycle: "h12",
438
+ timeZone: "UTC"
439
+ }).formatToParts(new Date(Date.UTC(2021, 4, 6, hour))).find((p) => p.type === "dayPeriod")?.value.toLowerCase().replace(/[.\s\u00a0\u202f]/g, "") ?? "";
440
+ const am = word(9);
441
+ const pm = word(15);
442
+ return am && pm && am !== pm ? {
443
+ am,
444
+ pm
445
+ } : null;
446
+ } catch {
447
+ return null;
448
+ }
449
+ }
450
+ function parseTimeInput(text, locale) {
451
+ let typed = normalizeDigits(text).trim().toLowerCase();
452
+ const words = dayPeriodWords(locale);
453
+ if (words) {
454
+ const compact = typed.replace(/[.\s\u00a0\u202f]/g, "");
455
+ if (compact.endsWith(words.pm)) typed = `${compact.slice(0, -words.pm.length)} pm`;
456
+ else if (compact.endsWith(words.am)) typed = `${compact.slice(0, -words.am.length)} am`;
457
+ else if (compact.startsWith(words.pm)) typed = `${compact.slice(words.pm.length)} pm`;
458
+ else if (compact.startsWith(words.am)) typed = `${compact.slice(words.am.length)} am`;
459
+ }
460
+ const m = /^(\d{1,2})[:.]?(\d{2})\s*([ap])\.?m\.?$|^(\d{1,2})[:.]?(\d{2})$/.exec(typed);
461
+ if (!m) return null;
462
+ let hour = Number(m[1] ?? m[4]);
463
+ const minute = Number(m[2] ?? m[5]);
464
+ const meridiem = m[3];
465
+ if (meridiem === "p" && hour < 12) hour += 12;
466
+ if (meridiem === "a" && hour === 12) hour = 0;
467
+ if (hour > 23 || minute > 59) return null;
468
+ return formatIsoTime({
469
+ hour,
470
+ minute
471
+ });
386
472
  }
387
- /** Does this locale write times on a 12-hour clock? */
388
473
  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
- }
474
+ try {
475
+ return new Intl.DateTimeFormat(loc, {
476
+ hour: "numeric",
477
+ timeZone: "UTC"
478
+ }).formatToParts(new Date(Date.UTC(2021, 0, 1, 13))).some((p) => p.type === "dayPeriod");
479
+ } catch {
480
+ return false;
481
+ }
399
482
  }
400
- /** The host's current calendar day, read via the *local* getters. */
401
483
  function todayIso() {
402
- const now = new Date();
403
- return formatIsoDate({
404
- year: now.getFullYear(),
405
- month: now.getMonth() + 1,
406
- day: now.getDate(),
407
- });
484
+ const now = /* @__PURE__ */ new Date();
485
+ return formatIsoDate({
486
+ year: now.getFullYear(),
487
+ month: now.getMonth() + 1,
488
+ day: now.getDate()
489
+ });
408
490
  }
409
491
  let uid = 0;
410
- /** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */
411
492
  function nextDateTimePickerId() {
412
- uid += 1;
413
- return `date-time-picker-${uid}`;
414
- }
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.7", ngImport: i0, type: DateTimePickerIcon, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
433
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: DateTimePickerIcon, isStandalone: true, selector: "ng-template[lilyDateTimePickerIcon]", ngImport: i0 }); }
493
+ uid += 1;
494
+ return `date-time-picker-${uid}`;
434
495
  }
435
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", 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.7", ngImport: i0, type: DateTimePicker, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1389
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", 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: `
496
+ var DateTimePickerIcon = class DateTimePickerIcon {
497
+ static ngTemplateContextGuard(_dir, _ctx) {
498
+ return true;
499
+ }
500
+ static {
501
+ this.ɵfac = i0.ɵɵngDeclareFactory({
502
+ minVersion: "12.0.0",
503
+ version: "22.2.4",
504
+ ngImport: i0,
505
+ type: DateTimePickerIcon,
506
+ deps: [],
507
+ target: i0.ɵɵFactoryTarget.Directive
508
+ });
509
+ }
510
+ static {
511
+ this.ɵdir = i0.ɵɵngDeclareDirective({
512
+ minVersion: "14.0.0",
513
+ version: "22.2.4",
514
+ type: DateTimePickerIcon,
515
+ isStandalone: true,
516
+ selector: "ng-template[lilyDateTimePickerIcon]",
517
+ ngImport: i0
518
+ });
519
+ }
520
+ };
521
+ i0.ɵɵngDeclareClassMetadata({
522
+ minVersion: "12.0.0",
523
+ version: "22.2.4",
524
+ ngImport: i0,
525
+ type: DateTimePickerIcon,
526
+ decorators: [{
527
+ type: Directive,
528
+ args: [{
529
+ selector: "ng-template[lilyDateTimePickerIcon]",
530
+ standalone: true
531
+ }]
532
+ }]
533
+ });
534
+ var DateTimePicker = class DateTimePicker {
535
+ onTooltipButtonEnter() {
536
+ this.hoverButton.set(true);
537
+ this.dismissed.set(false);
538
+ }
539
+ onTooltipButtonFocusIn() {
540
+ try {
541
+ this.focusButton.set(this.buttonRef().element?.matches(":focus-visible") ?? false);
542
+ } catch {
543
+ this.focusButton.set(true);
544
+ }
545
+ }
546
+ onTooltipButtonFocusOut() {
547
+ this.focusButton.set(false);
548
+ this.dismissed.set(false);
549
+ }
550
+ onTooltipKeydown(event) {
551
+ if (event.key === "Escape" && this.tooltipVisible()) this.dismissed.set(true);
552
+ }
553
+ constructor() {
554
+ this.label = input.required(...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
555
+ this.labels = input.required(...ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []);
556
+ this.mode = input("date", ...ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []);
557
+ this.value = model("", ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
558
+ this.locale = input("", ...ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []);
559
+ this.calendar = input("gregory", ...ngDevMode ? [{ debugName: "calendar" }] : /* istanbul ignore next */ []);
560
+ this.min = input("", ...ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []);
561
+ this.max = input("", ...ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []);
562
+ this.isDateDisabled = input(void 0, ...ngDevMode ? [{ debugName: "isDateDisabled" }] : /* istanbul ignore next */ []);
563
+ this.firstDayOfWeek = input(void 0, ...ngDevMode ? [{ debugName: "firstDayOfWeek" }] : /* istanbul ignore next */ []);
564
+ this.minuteStep = input(1, ...ngDevMode ? [{ debugName: "minuteStep" }] : /* istanbul ignore next */ []);
565
+ this.hour12 = input(void 0, ...ngDevMode ? [{ debugName: "hour12" }] : /* istanbul ignore next */ []);
566
+ this.showWeekNumbers = input(false, ...ngDevMode ? [{ debugName: "showWeekNumbers" }] : /* istanbul ignore next */ []);
567
+ this.shortcuts = input([], ...ngDevMode ? [{ debugName: "shortcuts" }] : /* istanbul ignore next */ []);
568
+ this.confirmOnSelect = input(void 0, ...ngDevMode ? [{ debugName: "confirmOnSelect" }] : /* istanbul ignore next */ []);
569
+ this.name = input("date-time", ...ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []);
570
+ this.timeZone = model("", ...ngDevMode ? [{ debugName: "timeZone" }] : /* istanbul ignore next */ []);
571
+ this.timeZones = input(void 0, ...ngDevMode ? [{ debugName: "timeZones" }] : /* istanbul ignore next */ []);
572
+ this.timeZoneLabels = input({}, ...ngDevMode ? [{ debugName: "timeZoneLabels" }] : /* istanbul ignore next */ []);
573
+ this.inputId = input("", ...ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []);
574
+ this.describedBy = input("", ...ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []);
575
+ this.placeholder = input("", ...ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []);
576
+ this.disabled = input(false, ...ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []);
577
+ this.readOnly = input(false, ...ngDevMode ? [{ debugName: "readOnly" }] : /* istanbul ignore next */ []);
578
+ this.required = input(false, ...ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []);
579
+ this.formatValue = input(void 0, ...ngDevMode ? [{ debugName: "formatValue" }] : /* istanbul ignore next */ []);
580
+ this.parseInput = input(void 0, ...ngDevMode ? [{ debugName: "parseInput" }] : /* istanbul ignore next */ []);
581
+ this.className = input("", ...ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []);
582
+ this.change = output();
583
+ this.shortcut = output();
584
+ this.invalidInput = output();
585
+ this.iconTemplate = contentChild(TemplateRef, ...ngDevMode ? [{ debugName: "iconTemplate" }] : /* istanbul ignore next */ []);
586
+ this.rootRef = viewChild.required("rootEl", ...ngDevMode ? [{ debugName: "rootRef" }] : /* istanbul ignore next */ []);
587
+ this.buttonRef = viewChild.required("buttonEl", ...ngDevMode ? [{ debugName: "buttonRef" }] : /* istanbul ignore next */ []);
588
+ this.dialogRef = viewChild.required("dialogEl", ...ngDevMode ? [{ debugName: "dialogRef" }] : /* istanbul ignore next */ []);
589
+ this.gridRef = viewChild("gridEl", ...ngDevMode ? [{ debugName: "gridRef" }] : /* istanbul ignore next */ []);
590
+ this.glyph = CALENDAR;
591
+ this.cdr = inject(ChangeDetectorRef);
592
+ this.baseId = nextDateTimePickerId();
593
+ this.dialogId = `${this.baseId}-dialog`;
594
+ this.periodId = `${this.baseId}-period`;
595
+ this.hourId = `${this.baseId}-hour`;
596
+ this.minuteId = `${this.baseId}-minute`;
597
+ this.meridiemId = `${this.baseId}-meridiem`;
598
+ this.statusId = `${this.baseId}-status`;
599
+ this.instructionsId = `${this.baseId}-instructions`;
600
+ this.timeZoneId = `${this.baseId}-time-zone`;
601
+ this.open = signal(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
602
+ this.tooltipId = `${this.baseId}-tooltip`;
603
+ this.hoverButton = signal(false, ...ngDevMode ? [{ debugName: "hoverButton" }] : /* istanbul ignore next */ []);
604
+ this.hoverTooltip = signal(false, ...ngDevMode ? [{ debugName: "hoverTooltip" }] : /* istanbul ignore next */ []);
605
+ this.focusButton = signal(false, ...ngDevMode ? [{ debugName: "focusButton" }] : /* istanbul ignore next */ []);
606
+ this.dismissed = signal(false, ...ngDevMode ? [{ debugName: "dismissed" }] : /* istanbul ignore next */ []);
607
+ this.tooltipVisible = computed(() => !this.open() && !this.dismissed() && (this.hoverButton() || this.hoverTooltip() || this.focusButton()), ...ngDevMode ? [{ debugName: "tooltipVisible" }] : /* istanbul ignore next */ []);
608
+ this.tooltipEscapeListener = effect((onCleanup) => {
609
+ if (!this.tooltipVisible() || typeof document === "undefined") return;
610
+ const onKey = (event) => {
611
+ if (event.key === "Escape") this.dismissed.set(true);
612
+ };
613
+ document.addEventListener("keydown", onKey);
614
+ onCleanup(() => document.removeEventListener("keydown", onKey));
615
+ }, ...ngDevMode ? [{ debugName: "tooltipEscapeListener" }] : /* istanbul ignore next */ []);
616
+ this.invalid = signal(false, ...ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []);
617
+ this.openerEl = null;
618
+ this.typed = signal(null, ...ngDevMode ? [{ debugName: "typed" }] : /* istanbul ignore next */ []);
619
+ this.pendingDate = signal("", ...ngDevMode ? [{ debugName: "pendingDate" }] : /* istanbul ignore next */ []);
620
+ this.pendingTime = signal("", ...ngDevMode ? [{ debugName: "pendingTime" }] : /* istanbul ignore next */ []);
621
+ this.viewStart = signal("1970-01-01", ...ngDevMode ? [{ debugName: "viewStart" }] : /* istanbul ignore next */ []);
622
+ this.cursor = signal("", ...ngDevMode ? [{ debugName: "cursor" }] : /* istanbul ignore next */ []);
623
+ this.today = signal("", ...ngDevMode ? [{ debugName: "today" }] : /* istanbul ignore next */ []);
624
+ this.fieldId = computed(() => this.inputId() || `${this.baseId}-input`, ...ngDevMode ? [{ debugName: "fieldId" }] : /* istanbul ignore next */ []);
625
+ this.committed = computed(() => splitValue(this.value(), this.mode()), ...ngDevMode ? [{ debugName: "committed" }] : /* istanbul ignore next */ []);
626
+ this.weekStart = computed(() => this.firstDayOfWeek() ?? firstDayOfWeekFor(this.locale() || void 0), ...ngDevMode ? [{ debugName: "weekStart" }] : /* istanbul ignore next */ []);
627
+ this.commitOnDay = computed(() => this.confirmOnSelect() ?? this.mode() === "date", ...ngDevMode ? [{ debugName: "commitOnDay" }] : /* istanbul ignore next */ []);
628
+ this.usesDate = computed(() => this.mode() !== "time", ...ngDevMode ? [{ debugName: "usesDate" }] : /* istanbul ignore next */ []);
629
+ this.usesTime = computed(() => this.mode() !== "date", ...ngDevMode ? [{ debugName: "usesTime" }] : /* istanbul ignore next */ []);
630
+ this.clock12 = computed(() => this.hour12() ?? localeUsesHour12(this.locale() || void 0), ...ngDevMode ? [{ debugName: "clock12" }] : /* istanbul ignore next */ []);
631
+ 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 */ []);
632
+ this.display = computed(() => {
633
+ const t = this.typed();
634
+ if (t !== null) return t;
635
+ return (this.formatValue() ?? ((v) => this.defaultFormat(v)))(this.value());
636
+ }, ...ngDevMode ? [{ debugName: "display" }] : /* istanbul ignore next */ []);
637
+ this.zoneOptions = computed(() => {
638
+ if (this.timeZones() !== void 0) return this.timeZones();
639
+ return typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("timeZone") : [];
640
+ }, ...ngDevMode ? [{ debugName: "zoneOptions" }] : /* istanbul ignore next */ []);
641
+ this.periodText = computed(() => {
642
+ const viewStart = this.viewStart();
643
+ try {
644
+ return new Intl.DateTimeFormat(calendarLocale(this.locale() || void 0, this.calendar()), {
645
+ month: "long",
646
+ year: "numeric",
647
+ timeZone: "UTC"
648
+ }).format(/* @__PURE__ */ new Date(`${viewStart}T00:00:00Z`));
649
+ } catch {
650
+ return viewStart.slice(0, 7);
651
+ }
652
+ }, ...ngDevMode ? [{ debugName: "periodText" }] : /* istanbul ignore next */ []);
653
+ this.weekdays = computed(() => {
654
+ const weekStart = this.weekStart();
655
+ const locale = this.locale() || void 0;
656
+ const out = [];
657
+ for (let i = 0; i < 7; i++) {
658
+ const d = new Date(Date.UTC(2021, 7, 1 + (weekStart + i) % 7));
659
+ try {
660
+ out.push({
661
+ short: new Intl.DateTimeFormat(gregorian(locale), {
662
+ weekday: "short",
663
+ timeZone: "UTC"
664
+ }).format(d),
665
+ long: new Intl.DateTimeFormat(gregorian(locale), {
666
+ weekday: "long",
667
+ timeZone: "UTC"
668
+ }).format(d)
669
+ });
670
+ } catch {
671
+ out.push({
672
+ short: "",
673
+ long: ""
674
+ });
675
+ }
676
+ }
677
+ return out;
678
+ }, ...ngDevMode ? [{ debugName: "weekdays" }] : /* istanbul ignore next */ []);
679
+ this.weeks = computed(() => calendarMonthMatrix(this.viewStart(), this.weekStart(), this.calendar()), ...ngDevMode ? [{ debugName: "weeks" }] : /* istanbul ignore next */ []);
680
+ this.pendingHour = computed(() => parseIsoTime(this.pendingTime())?.hour ?? 0, ...ngDevMode ? [{ debugName: "pendingHour" }] : /* istanbul ignore next */ []);
681
+ this.pendingMinute = computed(() => parseIsoTime(this.pendingTime())?.minute ?? 0, ...ngDevMode ? [{ debugName: "pendingMinute" }] : /* istanbul ignore next */ []);
682
+ this.hourOptions = computed(() => {
683
+ const clock12 = this.clock12();
684
+ const pendingHour = this.pendingHour();
685
+ const out = [];
686
+ for (let h = 0; h < 24; h++) {
687
+ if (clock12 && h < 12 !== pendingHour < 12) continue;
688
+ out.push({
689
+ value: h,
690
+ label: clock12 ? String((h + 11) % 12 + 1) : pad(h)
691
+ });
692
+ }
693
+ return out;
694
+ }, ...ngDevMode ? [{ debugName: "hourOptions" }] : /* istanbul ignore next */ []);
695
+ this.minuteOptions = computed(() => {
696
+ const step = Math.max(1, this.minuteStep());
697
+ const out = [];
698
+ for (let m = 0; m < 60; m += step) out.push(m);
699
+ return out;
700
+ }, ...ngDevMode ? [{ debugName: "minuteOptions" }] : /* istanbul ignore next */ []);
701
+ this.childContext = computed(() => {
702
+ const args = {
703
+ value: this.value(),
704
+ open: this.open(),
705
+ display: this.display()
706
+ };
707
+ return {
708
+ $implicit: args,
709
+ ...args
710
+ };
711
+ }, ...ngDevMode ? [{ debugName: "childContext" }] : /* istanbul ignore next */ []);
712
+ this.initialised = false;
713
+ effect(() => {
714
+ const committed = this.committed();
715
+ if (this.initialised) return;
716
+ this.initialised = true;
717
+ const today = todayIso();
718
+ this.today.set(today);
719
+ const anchor = parseIsoDate(committed.date) ?? parseIsoDate(today);
720
+ if (anchor) {
721
+ this.viewStart.set(calendarMonthStart(formatIsoDate(anchor), this.calendar()));
722
+ this.cursor.set(formatIsoDate(anchor));
723
+ }
724
+ });
725
+ const destroyRef = inject(DestroyRef);
726
+ afterNextRender(() => {
727
+ const root = this.rootRef().nativeElement;
728
+ const form = root.closest("form");
729
+ if (!form) return;
730
+ const initialValue = this.value();
731
+ const initialZone = this.timeZone();
732
+ let timer;
733
+ const onReset = (event) => {
734
+ clearTimeout(timer);
735
+ timer = setTimeout(() => {
736
+ if (event.defaultPrevented) return;
737
+ this.closeDialog(false);
738
+ this.typed.set(null);
739
+ this.invalid.set(false);
740
+ this.value.set(initialValue);
741
+ this.timeZone.set(initialZone);
742
+ const field = root.querySelector(".date-time-picker-input");
743
+ const formatter = this.formatValue() ?? ((v) => this.defaultFormat(v));
744
+ if (field) field.value = formatter(initialValue ?? "");
745
+ this.cdr.markForCheck();
746
+ }, 0);
747
+ };
748
+ form.addEventListener("reset", onReset);
749
+ destroyRef.onDestroy(() => {
750
+ clearTimeout(timer);
751
+ form.removeEventListener("reset", onReset);
752
+ });
753
+ });
754
+ }
755
+ pad2(n) {
756
+ return pad(n);
757
+ }
758
+ isoWeekOf(isoDate) {
759
+ return isoWeek(isoDate);
760
+ }
761
+ parseCell(isoDate) {
762
+ return {
763
+ day: parseIsoDate(isoDate) ? String(calendarDay(isoDate, this.calendar())) : "",
764
+ outside: isoDate < this.viewStart() || isoDate >= calendarAddMonths(this.viewStart(), 1, this.calendar())
765
+ };
766
+ }
767
+ dayLabel(isoDate) {
768
+ const parsed = parseIsoDate(isoDate);
769
+ if (!parsed) return isoDate;
770
+ try {
771
+ return new Intl.DateTimeFormat(calendarLocale(this.locale() || void 0, this.calendar()), {
772
+ weekday: "long",
773
+ day: "numeric",
774
+ month: "long",
775
+ year: "numeric",
776
+ timeZone: "UTC"
777
+ }).format(new Date(Date.UTC(parsed.year, parsed.month - 1, parsed.day)));
778
+ } catch {
779
+ return isoDate;
780
+ }
781
+ }
782
+ dayPeriodName(pm) {
783
+ try {
784
+ const found = new Intl.DateTimeFormat(gregorian(this.locale() || void 0), {
785
+ hour: "numeric",
786
+ hour12: true,
787
+ timeZone: "UTC"
788
+ }).formatToParts(new Date(Date.UTC(2021, 0, 1, pm ? 13 : 1))).find((p) => p.type === "dayPeriod")?.value;
789
+ if (found) return found;
790
+ } catch {}
791
+ return pm ? "PM" : "AM";
792
+ }
793
+ formatTimeForDisplay(isoTime) {
794
+ const parsed = parseIsoTime(isoTime);
795
+ if (!parsed) return isoTime;
796
+ try {
797
+ return new Intl.DateTimeFormat(gregorian(this.locale() || void 0), {
798
+ hour: "2-digit",
799
+ minute: "2-digit",
800
+ hour12: this.clock12(),
801
+ timeZone: "UTC"
802
+ }).format(new Date(Date.UTC(2021, 0, 1, parsed.hour, parsed.minute)));
803
+ } catch {
804
+ return isoTime;
805
+ }
806
+ }
807
+ defaultFormat(isoValue) {
808
+ if (!isoValue) return "";
809
+ const { date, time } = splitValue(isoValue, this.mode());
810
+ const chunks = [];
811
+ const parsed = date ? parseIsoDate(date) : null;
812
+ if (parsed) try {
813
+ chunks.push(new Intl.DateTimeFormat(gregorian(this.locale() || void 0), {
814
+ day: "2-digit",
815
+ month: "short",
816
+ year: "numeric",
817
+ timeZone: "UTC"
818
+ }).format(new Date(Date.UTC(parsed.year, parsed.month - 1, parsed.day))));
819
+ } catch {
820
+ chunks.push(date);
821
+ }
822
+ if (time) chunks.push(this.formatTimeForDisplay(time));
823
+ return chunks.join(" ");
824
+ }
825
+ dayDisabled(isoDate) {
826
+ if (!withinRange(isoDate, this.min() || void 0, this.max() || void 0)) return true;
827
+ return this.isDateDisabled()?.(isoDate) === true;
828
+ }
829
+ nearestSelectable(isoDate) {
830
+ if (!this.dayDisabled(isoDate)) return isoDate;
831
+ for (let delta = 1; delta <= 366; delta++) {
832
+ const after = addDays(isoDate, delta);
833
+ if (!this.dayDisabled(after)) return after;
834
+ const before = addDays(isoDate, -delta);
835
+ if (!this.dayDisabled(before)) return before;
836
+ }
837
+ return isoDate;
838
+ }
839
+ openDialog() {
840
+ if (this.disabled() || this.readOnly()) return;
841
+ const active = document.activeElement;
842
+ this.openerEl = active instanceof HTMLElement && this.rootRef().nativeElement.contains(active) ? active : this.buttonRef().element ?? null;
843
+ const today = todayIso();
844
+ this.today.set(today);
845
+ const pendingDate = this.committed().date || this.nearestSelectable(today);
846
+ this.pendingDate.set(pendingDate);
847
+ this.pendingTime.set(this.committed().time || this.defaultTime());
848
+ this.cursor.set(pendingDate);
849
+ const anchor = parseIsoDate(pendingDate) ?? parseIsoDate(today);
850
+ if (anchor) this.viewStart.set(calendarMonthStart(formatIsoDate(anchor), this.calendar()));
851
+ this.open.set(true);
852
+ this.cdr.detectChanges();
853
+ this.focusInitial();
854
+ }
855
+ defaultTime() {
856
+ if (!this.usesTime()) return "";
857
+ const now = /* @__PURE__ */ new Date();
858
+ const step = Math.max(1, this.minuteStep());
859
+ return formatIsoTime({
860
+ hour: now.getHours(),
861
+ minute: Math.floor(now.getMinutes() / step) * step
862
+ });
863
+ }
864
+ focusInitial() {
865
+ if (this.usesDate()) {
866
+ this.focusCursor();
867
+ return;
868
+ }
869
+ this.focusables()[0]?.focus?.();
870
+ }
871
+ closeDialog(refocus = true) {
872
+ if (!this.open()) return;
873
+ this.open.set(false);
874
+ if (refocus) {
875
+ const target = this.openerEl ?? this.buttonRef().element;
876
+ queueMicrotask(() => target?.focus?.());
877
+ }
878
+ }
879
+ commit() {
880
+ const next = joinValue(this.pendingDate(), this.pendingTime(), this.mode());
881
+ if (!next) return;
882
+ this.typed.set(null);
883
+ this.invalid.set(false);
884
+ if (next !== this.value()) {
885
+ this.value.set(next);
886
+ this.change.emit(next);
887
+ }
888
+ this.closeDialog();
889
+ }
890
+ clear() {
891
+ this.typed.set(null);
892
+ this.invalid.set(false);
893
+ if (this.value() !== "") {
894
+ this.value.set("");
895
+ this.change.emit("");
896
+ }
897
+ this.closeDialog();
898
+ }
899
+ moveCursor(nextIso) {
900
+ if (!withinRange(nextIso, this.min() || void 0, this.max() || void 0)) return;
901
+ this.cursor.set(nextIso);
902
+ const parsed = parseIsoDate(nextIso);
903
+ if (parsed) this.viewStart.set(calendarMonthStart(formatIsoDate(parsed), this.calendar()));
904
+ this.cdr.detectChanges();
905
+ this.focusCursor();
906
+ }
907
+ focusCursor() {
908
+ const dialogEl = this.dialogRef()?.nativeElement;
909
+ const cursor = this.cursor();
910
+ if (!dialogEl || !cursor) return;
911
+ const el = dialogEl.querySelector(`[data-date="${cursor}"]`);
912
+ el?.focus?.();
913
+ el?.scrollIntoView?.({ block: "nearest" });
914
+ }
915
+ selectDay(isoDate) {
916
+ if (this.dayDisabled(isoDate)) return;
917
+ this.pendingDate.set(isoDate);
918
+ this.cursor.set(isoDate);
919
+ if (this.commitOnDay()) this.commit();
920
+ }
921
+ shiftMonth(delta) {
922
+ const next = calendarAddMonths(this.viewStart(), delta, this.calendar());
923
+ const hadGridFocus = this.gridRef()?.nativeElement.contains(document.activeElement) === true;
924
+ this.viewStart.set(next);
925
+ const c = parseIsoDate(this.cursor());
926
+ if (c) {
927
+ this.cursor.set(addDays(next, Math.min(calendarDay(formatIsoDate(c), this.calendar()), calendarMonthLength(next, this.calendar())) - 1));
928
+ if (hadGridFocus) {
929
+ this.cdr.detectChanges();
930
+ this.focusCursor();
931
+ }
932
+ }
933
+ }
934
+ shiftYear(delta) {
935
+ this.shiftMonth(delta * 12);
936
+ }
937
+ shiftDays(delta) {
938
+ const from = parseIsoDate(this.cursor()) ? this.cursor() : this.pendingDate();
939
+ if (!from) return;
940
+ const next = addDays(from, delta);
941
+ if (!withinRange(next, this.min() || void 0, this.max() || void 0)) return;
942
+ const hadGridFocus = this.gridRef()?.nativeElement.contains(document.activeElement) === true;
943
+ const parsed = parseIsoDate(next);
944
+ if (parsed) this.viewStart.set(calendarMonthStart(formatIsoDate(parsed), this.calendar()));
945
+ this.cursor.set(next);
946
+ if (!this.dayDisabled(next)) this.pendingDate.set(next);
947
+ if (hadGridFocus) {
948
+ this.cdr.detectChanges();
949
+ this.focusCursor();
950
+ }
951
+ }
952
+ onTimeZoneSelect(event) {
953
+ this.timeZone.set(event.target.value);
954
+ }
955
+ zoneLabel(zone) {
956
+ return this.timeZoneLabels()[zone] ?? zone;
957
+ }
958
+ onGridKeydown(event) {
959
+ switch (logicalKey(event.key, isRtl(this.gridRef()?.nativeElement))) {
960
+ case "ArrowLeft":
961
+ event.preventDefault();
962
+ this.moveCursor(addDays(this.cursor(), -1));
963
+ break;
964
+ case "ArrowRight":
965
+ event.preventDefault();
966
+ this.moveCursor(addDays(this.cursor(), 1));
967
+ break;
968
+ case "ArrowUp":
969
+ event.preventDefault();
970
+ this.moveCursor(addDays(this.cursor(), -7));
971
+ break;
972
+ case "ArrowDown":
973
+ event.preventDefault();
974
+ this.moveCursor(addDays(this.cursor(), 7));
975
+ break;
976
+ case "Home": {
977
+ event.preventDefault();
978
+ const offset = (weekdayOf(this.cursor()) - this.weekStart() + 7) % 7;
979
+ this.moveCursor(addDays(this.cursor(), -offset));
980
+ break;
981
+ }
982
+ case "End": {
983
+ event.preventDefault();
984
+ const offset = (weekdayOf(this.cursor()) - this.weekStart() + 7) % 7;
985
+ this.moveCursor(addDays(this.cursor(), 6 - offset));
986
+ break;
987
+ }
988
+ case "PageUp":
989
+ event.preventDefault();
990
+ if (event.shiftKey) this.shiftYear(-1);
991
+ else this.shiftMonth(-1);
992
+ break;
993
+ case "PageDown":
994
+ event.preventDefault();
995
+ if (event.shiftKey) this.shiftYear(1);
996
+ else this.shiftMonth(1);
997
+ break;
998
+ case "Enter":
999
+ case " ":
1000
+ event.preventDefault();
1001
+ this.selectDay(this.cursor());
1002
+ }
1003
+ }
1004
+ focusables() {
1005
+ const dialogEl = this.dialogRef()?.nativeElement;
1006
+ if (!dialogEl) return [];
1007
+ return Array.from(dialogEl.querySelectorAll("button:not([disabled]):not([tabindex=\"-1\"]), select:not([disabled])"));
1008
+ }
1009
+ onDialogKeydown(event) {
1010
+ if (event.key === "Escape") {
1011
+ event.preventDefault();
1012
+ this.closeDialog();
1013
+ return;
1014
+ }
1015
+ if (event.key !== "Tab") return;
1016
+ const all = this.focusables();
1017
+ if (all.length === 0) return;
1018
+ const first = all[0];
1019
+ const last = all[all.length - 1];
1020
+ const active = document.activeElement;
1021
+ const dialogEl = this.dialogRef()?.nativeElement;
1022
+ if (event.shiftKey && (active === first || !dialogEl?.contains(active))) {
1023
+ event.preventDefault();
1024
+ last.focus?.();
1025
+ } else if (!event.shiftKey && active === last) {
1026
+ event.preventDefault();
1027
+ first.focus?.();
1028
+ }
1029
+ }
1030
+ parseTypedForMode(text) {
1031
+ if (this.mode() === "time") return parseTimeInput(text, this.locale() || void 0);
1032
+ if (this.mode() === "date") return parseDateInput(text, this.locale() || void 0);
1033
+ const m = /^(.*?)[T\s]+([^\sT]+)$/.exec(text.trim());
1034
+ if (!m) return null;
1035
+ const date = parseDateInput(m[1], this.locale() || void 0);
1036
+ const time = parseTimeInput(m[2], this.locale() || void 0);
1037
+ return date && time ? `${date}T${time}` : null;
1038
+ }
1039
+ resolveTyped() {
1040
+ const text = this.typed();
1041
+ if (text === null) return;
1042
+ if (!text.trim()) {
1043
+ this.typed.set(null);
1044
+ this.invalid.set(false);
1045
+ if (this.value() !== "") {
1046
+ this.value.set("");
1047
+ this.change.emit("");
1048
+ }
1049
+ return;
1050
+ }
1051
+ const parser = this.parseInput();
1052
+ const parsed = parser ? parser(text) : this.parseTypedForMode(text);
1053
+ if (!parsed) {
1054
+ this.invalid.set(true);
1055
+ this.invalidInput.emit(text);
1056
+ return;
1057
+ }
1058
+ const { date } = splitValue(parsed, this.mode());
1059
+ if (this.usesDate() && date && this.dayDisabled(date)) {
1060
+ this.invalid.set(true);
1061
+ this.invalidInput.emit(text);
1062
+ return;
1063
+ }
1064
+ this.typed.set(null);
1065
+ this.invalid.set(false);
1066
+ if (parsed !== this.value()) {
1067
+ this.value.set(parsed);
1068
+ this.change.emit(parsed);
1069
+ }
1070
+ }
1071
+ onFieldKeydown(event) {
1072
+ if (event.key === "Enter") {
1073
+ event.preventDefault();
1074
+ this.resolveTyped();
1075
+ } else if (event.key === "ArrowDown" && event.altKey) {
1076
+ event.preventDefault();
1077
+ this.openDialog();
1078
+ } else if (event.key === "Escape" && this.typed() !== null) {
1079
+ event.preventDefault();
1080
+ event.stopPropagation();
1081
+ this.typed.set(null);
1082
+ this.invalid.set(false);
1083
+ }
1084
+ }
1085
+ setHour(hour) {
1086
+ this.pendingTime.set(formatIsoTime({
1087
+ hour,
1088
+ minute: this.pendingMinute()
1089
+ }));
1090
+ }
1091
+ setMinute(minute) {
1092
+ this.pendingTime.set(formatIsoTime({
1093
+ hour: this.pendingHour(),
1094
+ minute
1095
+ }));
1096
+ }
1097
+ setMeridiem(pm) {
1098
+ this.setHour(this.pendingHour() % 12 + (pm ? 12 : 0));
1099
+ }
1100
+ applyShortcut(shortcut) {
1101
+ const base = todayIso();
1102
+ let target = shortcut.date ?? base;
1103
+ if (shortcut.days !== void 0) target = addDays(base, shortcut.days);
1104
+ else if (shortcut.months !== void 0) target = addMonths(base, shortcut.months);
1105
+ if (this.dayDisabled(target)) return;
1106
+ this.pendingDate.set(target);
1107
+ this.cursor.set(target);
1108
+ const parsed = parseIsoDate(target);
1109
+ if (parsed) this.viewStart.set(calendarMonthStart(formatIsoDate(parsed), this.calendar()));
1110
+ this.shortcut.emit({
1111
+ id: shortcut.id,
1112
+ isoDate: target
1113
+ });
1114
+ if (this.commitOnDay()) this.commit();
1115
+ else {
1116
+ this.cdr.detectChanges();
1117
+ this.focusCursor();
1118
+ }
1119
+ }
1120
+ onDocumentClick(event) {
1121
+ if (!this.open()) return;
1122
+ const target = event.target;
1123
+ if (!target) return;
1124
+ if (this.dialogRef().nativeElement.contains(target) || (this.buttonRef().element?.contains(target) ?? false)) return;
1125
+ this.closeDialog(false);
1126
+ }
1127
+ static {
1128
+ this.ɵfac = i0.ɵɵngDeclareFactory({
1129
+ minVersion: "12.0.0",
1130
+ version: "22.2.4",
1131
+ ngImport: i0,
1132
+ type: DateTimePicker,
1133
+ deps: [],
1134
+ target: i0.ɵɵFactoryTarget.Component
1135
+ });
1136
+ }
1137
+ static {
1138
+ this.ɵcmp = i0.ɵɵngDeclareComponent({
1139
+ minVersion: "17.0.0",
1140
+ version: "22.2.4",
1141
+ type: DateTimePicker,
1142
+ isStandalone: true,
1143
+ selector: "lily-date-time-picker",
1144
+ inputs: {
1145
+ label: {
1146
+ classPropertyName: "label",
1147
+ publicName: "label",
1148
+ isSignal: true,
1149
+ isRequired: true,
1150
+ transformFunction: null
1151
+ },
1152
+ labels: {
1153
+ classPropertyName: "labels",
1154
+ publicName: "labels",
1155
+ isSignal: true,
1156
+ isRequired: true,
1157
+ transformFunction: null
1158
+ },
1159
+ mode: {
1160
+ classPropertyName: "mode",
1161
+ publicName: "mode",
1162
+ isSignal: true,
1163
+ isRequired: false,
1164
+ transformFunction: null
1165
+ },
1166
+ value: {
1167
+ classPropertyName: "value",
1168
+ publicName: "value",
1169
+ isSignal: true,
1170
+ isRequired: false,
1171
+ transformFunction: null
1172
+ },
1173
+ locale: {
1174
+ classPropertyName: "locale",
1175
+ publicName: "locale",
1176
+ isSignal: true,
1177
+ isRequired: false,
1178
+ transformFunction: null
1179
+ },
1180
+ calendar: {
1181
+ classPropertyName: "calendar",
1182
+ publicName: "calendar",
1183
+ isSignal: true,
1184
+ isRequired: false,
1185
+ transformFunction: null
1186
+ },
1187
+ min: {
1188
+ classPropertyName: "min",
1189
+ publicName: "min",
1190
+ isSignal: true,
1191
+ isRequired: false,
1192
+ transformFunction: null
1193
+ },
1194
+ max: {
1195
+ classPropertyName: "max",
1196
+ publicName: "max",
1197
+ isSignal: true,
1198
+ isRequired: false,
1199
+ transformFunction: null
1200
+ },
1201
+ isDateDisabled: {
1202
+ classPropertyName: "isDateDisabled",
1203
+ publicName: "isDateDisabled",
1204
+ isSignal: true,
1205
+ isRequired: false,
1206
+ transformFunction: null
1207
+ },
1208
+ firstDayOfWeek: {
1209
+ classPropertyName: "firstDayOfWeek",
1210
+ publicName: "firstDayOfWeek",
1211
+ isSignal: true,
1212
+ isRequired: false,
1213
+ transformFunction: null
1214
+ },
1215
+ minuteStep: {
1216
+ classPropertyName: "minuteStep",
1217
+ publicName: "minuteStep",
1218
+ isSignal: true,
1219
+ isRequired: false,
1220
+ transformFunction: null
1221
+ },
1222
+ hour12: {
1223
+ classPropertyName: "hour12",
1224
+ publicName: "hour12",
1225
+ isSignal: true,
1226
+ isRequired: false,
1227
+ transformFunction: null
1228
+ },
1229
+ showWeekNumbers: {
1230
+ classPropertyName: "showWeekNumbers",
1231
+ publicName: "showWeekNumbers",
1232
+ isSignal: true,
1233
+ isRequired: false,
1234
+ transformFunction: null
1235
+ },
1236
+ shortcuts: {
1237
+ classPropertyName: "shortcuts",
1238
+ publicName: "shortcuts",
1239
+ isSignal: true,
1240
+ isRequired: false,
1241
+ transformFunction: null
1242
+ },
1243
+ confirmOnSelect: {
1244
+ classPropertyName: "confirmOnSelect",
1245
+ publicName: "confirmOnSelect",
1246
+ isSignal: true,
1247
+ isRequired: false,
1248
+ transformFunction: null
1249
+ },
1250
+ name: {
1251
+ classPropertyName: "name",
1252
+ publicName: "name",
1253
+ isSignal: true,
1254
+ isRequired: false,
1255
+ transformFunction: null
1256
+ },
1257
+ timeZone: {
1258
+ classPropertyName: "timeZone",
1259
+ publicName: "timeZone",
1260
+ isSignal: true,
1261
+ isRequired: false,
1262
+ transformFunction: null
1263
+ },
1264
+ timeZones: {
1265
+ classPropertyName: "timeZones",
1266
+ publicName: "timeZones",
1267
+ isSignal: true,
1268
+ isRequired: false,
1269
+ transformFunction: null
1270
+ },
1271
+ timeZoneLabels: {
1272
+ classPropertyName: "timeZoneLabels",
1273
+ publicName: "timeZoneLabels",
1274
+ isSignal: true,
1275
+ isRequired: false,
1276
+ transformFunction: null
1277
+ },
1278
+ inputId: {
1279
+ classPropertyName: "inputId",
1280
+ publicName: "inputId",
1281
+ isSignal: true,
1282
+ isRequired: false,
1283
+ transformFunction: null
1284
+ },
1285
+ describedBy: {
1286
+ classPropertyName: "describedBy",
1287
+ publicName: "describedBy",
1288
+ isSignal: true,
1289
+ isRequired: false,
1290
+ transformFunction: null
1291
+ },
1292
+ placeholder: {
1293
+ classPropertyName: "placeholder",
1294
+ publicName: "placeholder",
1295
+ isSignal: true,
1296
+ isRequired: false,
1297
+ transformFunction: null
1298
+ },
1299
+ disabled: {
1300
+ classPropertyName: "disabled",
1301
+ publicName: "disabled",
1302
+ isSignal: true,
1303
+ isRequired: false,
1304
+ transformFunction: null
1305
+ },
1306
+ readOnly: {
1307
+ classPropertyName: "readOnly",
1308
+ publicName: "readOnly",
1309
+ isSignal: true,
1310
+ isRequired: false,
1311
+ transformFunction: null
1312
+ },
1313
+ required: {
1314
+ classPropertyName: "required",
1315
+ publicName: "required",
1316
+ isSignal: true,
1317
+ isRequired: false,
1318
+ transformFunction: null
1319
+ },
1320
+ formatValue: {
1321
+ classPropertyName: "formatValue",
1322
+ publicName: "formatValue",
1323
+ isSignal: true,
1324
+ isRequired: false,
1325
+ transformFunction: null
1326
+ },
1327
+ parseInput: {
1328
+ classPropertyName: "parseInput",
1329
+ publicName: "parseInput",
1330
+ isSignal: true,
1331
+ isRequired: false,
1332
+ transformFunction: null
1333
+ },
1334
+ className: {
1335
+ classPropertyName: "className",
1336
+ publicName: "className",
1337
+ isSignal: true,
1338
+ isRequired: false,
1339
+ transformFunction: null
1340
+ }
1341
+ },
1342
+ outputs: {
1343
+ value: "valueChange",
1344
+ timeZone: "timeZoneChange",
1345
+ change: "change",
1346
+ shortcut: "shortcut",
1347
+ invalidInput: "invalidInput"
1348
+ },
1349
+ host: { listeners: { "document:click": "onDocumentClick($event)" } },
1350
+ queries: [{
1351
+ propertyName: "iconTemplate",
1352
+ first: true,
1353
+ predicate: TemplateRef,
1354
+ descendants: true,
1355
+ isSignal: true
1356
+ }],
1357
+ viewQueries: [
1358
+ {
1359
+ propertyName: "rootRef",
1360
+ first: true,
1361
+ predicate: ["rootEl"],
1362
+ descendants: true,
1363
+ isSignal: true
1364
+ },
1365
+ {
1366
+ propertyName: "buttonRef",
1367
+ first: true,
1368
+ predicate: ["buttonEl"],
1369
+ descendants: true,
1370
+ isSignal: true
1371
+ },
1372
+ {
1373
+ propertyName: "dialogRef",
1374
+ first: true,
1375
+ predicate: ["dialogEl"],
1376
+ descendants: true,
1377
+ isSignal: true
1378
+ },
1379
+ {
1380
+ propertyName: "gridRef",
1381
+ first: true,
1382
+ predicate: ["gridEl"],
1383
+ descendants: true,
1384
+ isSignal: true
1385
+ }
1386
+ ],
1387
+ ngImport: i0,
1388
+ template: `
1390
1389
  <div
1391
1390
  #rootEl
1392
1391
  class="date-time-picker {{ className() }}"
@@ -1431,7 +1430,12 @@ class DateTimePicker {
1431
1430
  [ariaExpanded]="open()"
1432
1431
  [ariaControls]="dialogId"
1433
1432
  [disabled]="disabled() || readOnly()"
1434
- (click)="open() ? closeDialog() : openDialog()"
1433
+ (click)="open() ? closeDialog() : openDialog(); hoverButton.set(false)"
1434
+ (mouseenter)="onTooltipButtonEnter()"
1435
+ (mouseleave)="hoverButton.set(false)"
1436
+ (focusin)="onTooltipButtonFocusIn()"
1437
+ (focusout)="onTooltipButtonFocusOut()"
1438
+ (keydown)="onTooltipKeydown($event)"
1435
1439
  >
1436
1440
  @if (iconTemplate(); as tpl) {
1437
1441
  <ng-container
@@ -1444,6 +1448,18 @@ class DateTimePicker {
1444
1448
  }}</span>
1445
1449
  }
1446
1450
  </lily-icon-button>
1451
+
1452
+ <!-- Purely visual: the same text is already the button's aria-label,
1453
+ so it is not wired with aria-describedby (that would announce
1454
+ the name twice). -->
1455
+ <div
1456
+ class="date-time-picker-tooltip"
1457
+ role="tooltip"
1458
+ [id]="tooltipId"
1459
+ [attr.hidden]="tooltipVisible() ? null : ''"
1460
+ (mouseenter)="hoverTooltip.set(true)"
1461
+ (mouseleave)="hoverTooltip.set(false)"
1462
+ >{{ label() }}</div>
1447
1463
  </div>
1448
1464
 
1449
1465
  @if (labels().invalid) {
@@ -1759,19 +1775,50 @@ class DateTimePicker {
1759
1775
  </div>
1760
1776
  </div>
1761
1777
  </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", "tabIndex", "disabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1763
- }
1764
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", 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: `
1778
+ `,
1779
+ isInline: true,
1780
+ dependencies: [{
1781
+ kind: "directive",
1782
+ type: NgTemplateOutlet,
1783
+ selector: "[ngTemplateOutlet]",
1784
+ inputs: [
1785
+ "ngTemplateOutletContext",
1786
+ "ngTemplateOutlet",
1787
+ "ngTemplateOutletInjector"
1788
+ ]
1789
+ }, {
1790
+ kind: "component",
1791
+ type: IconButton,
1792
+ selector: "lily-icon-button",
1793
+ inputs: [
1794
+ "label",
1795
+ "className",
1796
+ "baseClass",
1797
+ "ariaHaspopup",
1798
+ "ariaExpanded",
1799
+ "ariaControls",
1800
+ "tabIndex",
1801
+ "disabled"
1802
+ ]
1803
+ }],
1804
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
1805
+ });
1806
+ }
1807
+ };
1808
+ i0.ɵɵngDeclareClassMetadata({
1809
+ minVersion: "12.0.0",
1810
+ version: "22.2.4",
1811
+ ngImport: i0,
1812
+ type: DateTimePicker,
1813
+ decorators: [{
1814
+ type: Component,
1815
+ args: [{
1816
+ selector: "lily-date-time-picker",
1817
+ standalone: true,
1818
+ imports: [NgTemplateOutlet, IconButton],
1819
+ changeDetection: ChangeDetectionStrategy.OnPush,
1820
+ host: { "(document:click)": "onDocumentClick($event)" },
1821
+ template: `
1775
1822
  <div
1776
1823
  #rootEl
1777
1824
  class="date-time-picker {{ className() }}"
@@ -1816,7 +1863,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1816
1863
  [ariaExpanded]="open()"
1817
1864
  [ariaControls]="dialogId"
1818
1865
  [disabled]="disabled() || readOnly()"
1819
- (click)="open() ? closeDialog() : openDialog()"
1866
+ (click)="open() ? closeDialog() : openDialog(); hoverButton.set(false)"
1867
+ (mouseenter)="onTooltipButtonEnter()"
1868
+ (mouseleave)="hoverButton.set(false)"
1869
+ (focusin)="onTooltipButtonFocusIn()"
1870
+ (focusout)="onTooltipButtonFocusOut()"
1871
+ (keydown)="onTooltipKeydown($event)"
1820
1872
  >
1821
1873
  @if (iconTemplate(); as tpl) {
1822
1874
  <ng-container
@@ -1829,6 +1881,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1829
1881
  }}</span>
1830
1882
  }
1831
1883
  </lily-icon-button>
1884
+
1885
+ <!-- Purely visual: the same text is already the button's aria-label,
1886
+ so it is not wired with aria-describedby (that would announce
1887
+ the name twice). -->
1888
+ <div
1889
+ class="date-time-picker-tooltip"
1890
+ role="tooltip"
1891
+ [id]="tooltipId"
1892
+ [attr.hidden]="tooltipVisible() ? null : ''"
1893
+ (mouseenter)="hoverTooltip.set(true)"
1894
+ (mouseleave)="hoverTooltip.set(false)"
1895
+ >{{ label() }}</div>
1832
1896
  </div>
1833
1897
 
1834
1898
  @if (labels().invalid) {
@@ -2144,13 +2208,275 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
2144
2208
  </div>
2145
2209
  </div>
2146
2210
  </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
- */
2211
+ `
2212
+ }]
2213
+ }],
2214
+ ctorParameters: () => [],
2215
+ propDecorators: {
2216
+ label: [{
2217
+ type: i0.Input,
2218
+ args: [{
2219
+ isSignal: true,
2220
+ alias: "label",
2221
+ required: true
2222
+ }]
2223
+ }],
2224
+ labels: [{
2225
+ type: i0.Input,
2226
+ args: [{
2227
+ isSignal: true,
2228
+ alias: "labels",
2229
+ required: true
2230
+ }]
2231
+ }],
2232
+ mode: [{
2233
+ type: i0.Input,
2234
+ args: [{
2235
+ isSignal: true,
2236
+ alias: "mode",
2237
+ required: false
2238
+ }]
2239
+ }],
2240
+ value: [{
2241
+ type: i0.Input,
2242
+ args: [{
2243
+ isSignal: true,
2244
+ alias: "value",
2245
+ required: false
2246
+ }]
2247
+ }, {
2248
+ type: i0.Output,
2249
+ args: ["valueChange"]
2250
+ }],
2251
+ locale: [{
2252
+ type: i0.Input,
2253
+ args: [{
2254
+ isSignal: true,
2255
+ alias: "locale",
2256
+ required: false
2257
+ }]
2258
+ }],
2259
+ calendar: [{
2260
+ type: i0.Input,
2261
+ args: [{
2262
+ isSignal: true,
2263
+ alias: "calendar",
2264
+ required: false
2265
+ }]
2266
+ }],
2267
+ min: [{
2268
+ type: i0.Input,
2269
+ args: [{
2270
+ isSignal: true,
2271
+ alias: "min",
2272
+ required: false
2273
+ }]
2274
+ }],
2275
+ max: [{
2276
+ type: i0.Input,
2277
+ args: [{
2278
+ isSignal: true,
2279
+ alias: "max",
2280
+ required: false
2281
+ }]
2282
+ }],
2283
+ isDateDisabled: [{
2284
+ type: i0.Input,
2285
+ args: [{
2286
+ isSignal: true,
2287
+ alias: "isDateDisabled",
2288
+ required: false
2289
+ }]
2290
+ }],
2291
+ firstDayOfWeek: [{
2292
+ type: i0.Input,
2293
+ args: [{
2294
+ isSignal: true,
2295
+ alias: "firstDayOfWeek",
2296
+ required: false
2297
+ }]
2298
+ }],
2299
+ minuteStep: [{
2300
+ type: i0.Input,
2301
+ args: [{
2302
+ isSignal: true,
2303
+ alias: "minuteStep",
2304
+ required: false
2305
+ }]
2306
+ }],
2307
+ hour12: [{
2308
+ type: i0.Input,
2309
+ args: [{
2310
+ isSignal: true,
2311
+ alias: "hour12",
2312
+ required: false
2313
+ }]
2314
+ }],
2315
+ showWeekNumbers: [{
2316
+ type: i0.Input,
2317
+ args: [{
2318
+ isSignal: true,
2319
+ alias: "showWeekNumbers",
2320
+ required: false
2321
+ }]
2322
+ }],
2323
+ shortcuts: [{
2324
+ type: i0.Input,
2325
+ args: [{
2326
+ isSignal: true,
2327
+ alias: "shortcuts",
2328
+ required: false
2329
+ }]
2330
+ }],
2331
+ confirmOnSelect: [{
2332
+ type: i0.Input,
2333
+ args: [{
2334
+ isSignal: true,
2335
+ alias: "confirmOnSelect",
2336
+ required: false
2337
+ }]
2338
+ }],
2339
+ name: [{
2340
+ type: i0.Input,
2341
+ args: [{
2342
+ isSignal: true,
2343
+ alias: "name",
2344
+ required: false
2345
+ }]
2346
+ }],
2347
+ timeZone: [{
2348
+ type: i0.Input,
2349
+ args: [{
2350
+ isSignal: true,
2351
+ alias: "timeZone",
2352
+ required: false
2353
+ }]
2354
+ }, {
2355
+ type: i0.Output,
2356
+ args: ["timeZoneChange"]
2357
+ }],
2358
+ timeZones: [{
2359
+ type: i0.Input,
2360
+ args: [{
2361
+ isSignal: true,
2362
+ alias: "timeZones",
2363
+ required: false
2364
+ }]
2365
+ }],
2366
+ timeZoneLabels: [{
2367
+ type: i0.Input,
2368
+ args: [{
2369
+ isSignal: true,
2370
+ alias: "timeZoneLabels",
2371
+ required: false
2372
+ }]
2373
+ }],
2374
+ inputId: [{
2375
+ type: i0.Input,
2376
+ args: [{
2377
+ isSignal: true,
2378
+ alias: "inputId",
2379
+ required: false
2380
+ }]
2381
+ }],
2382
+ describedBy: [{
2383
+ type: i0.Input,
2384
+ args: [{
2385
+ isSignal: true,
2386
+ alias: "describedBy",
2387
+ required: false
2388
+ }]
2389
+ }],
2390
+ placeholder: [{
2391
+ type: i0.Input,
2392
+ args: [{
2393
+ isSignal: true,
2394
+ alias: "placeholder",
2395
+ required: false
2396
+ }]
2397
+ }],
2398
+ disabled: [{
2399
+ type: i0.Input,
2400
+ args: [{
2401
+ isSignal: true,
2402
+ alias: "disabled",
2403
+ required: false
2404
+ }]
2405
+ }],
2406
+ readOnly: [{
2407
+ type: i0.Input,
2408
+ args: [{
2409
+ isSignal: true,
2410
+ alias: "readOnly",
2411
+ required: false
2412
+ }]
2413
+ }],
2414
+ required: [{
2415
+ type: i0.Input,
2416
+ args: [{
2417
+ isSignal: true,
2418
+ alias: "required",
2419
+ required: false
2420
+ }]
2421
+ }],
2422
+ formatValue: [{
2423
+ type: i0.Input,
2424
+ args: [{
2425
+ isSignal: true,
2426
+ alias: "formatValue",
2427
+ required: false
2428
+ }]
2429
+ }],
2430
+ parseInput: [{
2431
+ type: i0.Input,
2432
+ args: [{
2433
+ isSignal: true,
2434
+ alias: "parseInput",
2435
+ required: false
2436
+ }]
2437
+ }],
2438
+ className: [{
2439
+ type: i0.Input,
2440
+ args: [{
2441
+ isSignal: true,
2442
+ alias: "className",
2443
+ required: false
2444
+ }]
2445
+ }],
2446
+ change: [{
2447
+ type: i0.Output,
2448
+ args: ["change"]
2449
+ }],
2450
+ shortcut: [{
2451
+ type: i0.Output,
2452
+ args: ["shortcut"]
2453
+ }],
2454
+ invalidInput: [{
2455
+ type: i0.Output,
2456
+ args: ["invalidInput"]
2457
+ }],
2458
+ iconTemplate: [{
2459
+ type: i0.ContentChild,
2460
+ args: [i0.forwardRef(() => TemplateRef), { isSignal: true }]
2461
+ }],
2462
+ rootRef: [{
2463
+ type: i0.ViewChild,
2464
+ args: ["rootEl", { isSignal: true }]
2465
+ }],
2466
+ buttonRef: [{
2467
+ type: i0.ViewChild,
2468
+ args: ["buttonEl", { isSignal: true }]
2469
+ }],
2470
+ dialogRef: [{
2471
+ type: i0.ViewChild,
2472
+ args: ["dialogEl", { isSignal: true }]
2473
+ }],
2474
+ gridRef: [{
2475
+ type: i0.ViewChild,
2476
+ args: ["gridEl", { isSignal: true }]
2477
+ }]
2478
+ }
2479
+ });
2480
+ export { CALENDAR, DateTimePicker, DateTimePickerIcon, addDays, addMonths, daysInMonth, firstDayOfWeekFor, formatIsoDate, formatIsoTime, fromEpochDay, gregorian, isoWeek, joinValue, monthMatrix, monthNames, nextDateTimePickerId, normalizeDigits, numericFieldOrder, pad, parseDateInput, parseIsoDate, parseIsoTime, parseTimeInput, splitValue, toEpochDay, weekdayOf, withinRange };
2154
2481
 
2155
- 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
2482
+ //# sourceMappingURL=lilydesignsystem-angular-date-time-picker.mjs.map