@rastr/controls 0.0.0-stage → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,753 @@
1
+ // src/dom.ts
2
+ function h(tag, props = {}, ...children) {
3
+ const el = document.createElement(tag);
4
+ if (props.class) el.className = props.class;
5
+ for (const [name, value] of Object.entries(props.attrs ?? {})) setAttr(el, name, value);
6
+ if (props.text !== void 0) el.textContent = props.text;
7
+ for (const child of children) {
8
+ if (child !== null && child !== void 0 && child !== false) el.append(child);
9
+ }
10
+ return el;
11
+ }
12
+ function setAttr(el, name, value) {
13
+ if (value === false || value === null || value === void 0) el.removeAttribute(name);
14
+ else el.setAttribute(name, value === true ? "" : String(value));
15
+ }
16
+ var ids = 0;
17
+ var uniqueId = (prefix) => `${prefix}-${++ids}`;
18
+
19
+ // src/popover.ts
20
+ var GAP_PX = 4;
21
+ var VIEWPORT_MARGIN_PX = 8;
22
+ var POPOVER_OPEN_EVENT = "rastr:popover-open";
23
+ var current = null;
24
+ function closeControlPopover(within) {
25
+ if (current && (!within || within.contains(current.anchor) || within.contains(current.element))) current.close();
26
+ }
27
+ function openControlPopover(anchor, content, options) {
28
+ current?.close();
29
+ const host = anchor.closest("dialog, .rs-grid") ?? document.body;
30
+ const element = h("div", { class: "rs-control-popover" }, content);
31
+ host.append(element);
32
+ const position = () => {
33
+ const rect = anchor.getBoundingClientRect();
34
+ element.style.minWidth = `${rect.width}px`;
35
+ element.style.removeProperty("max-height");
36
+ const box = element.getBoundingClientRect();
37
+ const below = window.innerHeight - VIEWPORT_MARGIN_PX - (rect.bottom + GAP_PX);
38
+ const above = rect.top - GAP_PX - VIEWPORT_MARGIN_PX;
39
+ let top;
40
+ if (box.height <= below || box.height > above && below >= above) {
41
+ top = rect.bottom + GAP_PX;
42
+ if (box.height > below) element.style.maxHeight = `${Math.max(below, 0)}px`;
43
+ } else {
44
+ const height = Math.min(box.height, above);
45
+ if (box.height > above) element.style.maxHeight = `${Math.max(above, 0)}px`;
46
+ top = rect.top - GAP_PX - height;
47
+ }
48
+ const left = Math.max(VIEWPORT_MARGIN_PX, Math.min(rect.left, window.innerWidth - Math.max(box.width, rect.width) - VIEWPORT_MARGIN_PX));
49
+ element.style.top = `${top}px`;
50
+ element.style.left = `${left}px`;
51
+ };
52
+ let closed = false;
53
+ const onPointerDown = (event) => {
54
+ const target = event.target;
55
+ if (!anchor.isConnected || !element.contains(target) && !anchor.contains(target)) popover.close();
56
+ };
57
+ const onKeyDown = () => {
58
+ if (!anchor.isConnected) popover.close();
59
+ };
60
+ const onScroll = (event) => {
61
+ if (!(event.target instanceof Node && element.contains(event.target))) popover.close();
62
+ };
63
+ const onResize = () => popover.close();
64
+ const onFocusOut = (event) => {
65
+ const next = event.relatedTarget;
66
+ if (next instanceof Node && !element.contains(next) && !anchor.contains(next)) popover.close();
67
+ };
68
+ const onOtherOpen = (event) => {
69
+ if (event.detail !== element) popover.close();
70
+ };
71
+ const popover = {
72
+ element,
73
+ anchor,
74
+ close() {
75
+ if (closed) return;
76
+ closed = true;
77
+ if (current === popover) current = null;
78
+ document.removeEventListener("pointerdown", onPointerDown, true);
79
+ document.removeEventListener("keydown", onKeyDown, true);
80
+ document.removeEventListener("scroll", onScroll, true);
81
+ document.removeEventListener(POPOVER_OPEN_EVENT, onOtherOpen);
82
+ window.removeEventListener("resize", onResize);
83
+ element.removeEventListener("focusout", onFocusOut);
84
+ element.remove();
85
+ options.onClose();
86
+ }
87
+ };
88
+ position();
89
+ document.dispatchEvent(new CustomEvent(POPOVER_OPEN_EVENT, { detail: element }));
90
+ document.addEventListener("pointerdown", onPointerDown, true);
91
+ document.addEventListener("keydown", onKeyDown, true);
92
+ document.addEventListener("scroll", onScroll, true);
93
+ document.addEventListener(POPOVER_OPEN_EVENT, onOtherOpen);
94
+ window.addEventListener("resize", onResize);
95
+ element.addEventListener("focusout", onFocusOut);
96
+ current = popover;
97
+ return popover;
98
+ }
99
+
100
+ // src/select.ts
101
+ var TYPEAHEAD_MS = 600;
102
+ var PAGE = 10;
103
+ function createSelect(init) {
104
+ let options = init.options;
105
+ let value = init.value;
106
+ let popover = null;
107
+ let optionEls = [];
108
+ let active = -1;
109
+ let typed = "";
110
+ let typedAt = 0;
111
+ const listId = uniqueId("rs-select-list");
112
+ const label = h("span", { class: "rs-select__label" });
113
+ const element = h(
114
+ "button",
115
+ {
116
+ class: init.className ? `rs-select ${init.className}` : "rs-select",
117
+ attrs: { type: "button", role: "combobox", "aria-haspopup": "listbox", "aria-expanded": "false", "aria-label": init.label }
118
+ },
119
+ label
120
+ );
121
+ const render = () => {
122
+ const chosen = options.find((o) => o.value === value);
123
+ label.textContent = chosen?.label ?? init.placeholder ?? "";
124
+ setAttr(element, "data-value", value);
125
+ setAttr(element, "data-placeholder", chosen === void 0);
126
+ };
127
+ const enabledFrom = (index, step) => {
128
+ for (let i = Math.max(0, Math.min(index, options.length - 1)); i >= 0 && i < options.length; i += step) {
129
+ if (!options[i]?.disabled) return i;
130
+ }
131
+ return active;
132
+ };
133
+ const setActive = (index, step = 1) => {
134
+ if (optionEls.length === 0) return;
135
+ active = enabledFrom(index, step);
136
+ optionEls.forEach((el2, i) => setAttr(el2, "data-active", i === active));
137
+ const el = optionEls[active];
138
+ setAttr(element, "aria-activedescendant", el?.id);
139
+ el?.scrollIntoView?.({ block: "nearest" });
140
+ };
141
+ const close = () => popover?.close();
142
+ const choose = (index) => {
143
+ const option = options[index];
144
+ if (option?.disabled) return;
145
+ close();
146
+ element.focus();
147
+ if (!option) return;
148
+ const changed = option.value !== value;
149
+ value = option.value;
150
+ render();
151
+ if (changed) init.onChange(value);
152
+ init.onChoose?.(value);
153
+ };
154
+ const open = () => {
155
+ if (popover) return;
156
+ const list = h("div", { class: "rs-select__list", attrs: { role: "listbox", id: listId, "aria-label": init.label } });
157
+ optionEls = options.map((option, index) => {
158
+ const el = h("div", {
159
+ class: "rs-select__option",
160
+ attrs: {
161
+ role: "option",
162
+ id: `${listId}-${index}`,
163
+ "aria-selected": String(option.value === value),
164
+ "aria-disabled": option.disabled ? "true" : null,
165
+ "data-value": option.value,
166
+ title: option.title
167
+ },
168
+ text: option.label
169
+ });
170
+ el.addEventListener("pointerdown", (event) => event.preventDefault());
171
+ el.addEventListener("mousemove", () => {
172
+ if (!option.disabled) setActive(index);
173
+ });
174
+ el.addEventListener("click", () => choose(index));
175
+ return el;
176
+ });
177
+ list.append(...optionEls);
178
+ popover = openControlPopover(element, list, {
179
+ onClose: () => {
180
+ popover = null;
181
+ optionEls = [];
182
+ setAttr(element, "aria-expanded", "false");
183
+ setAttr(element, "aria-controls", null);
184
+ setAttr(element, "aria-activedescendant", null);
185
+ }
186
+ });
187
+ setAttr(element, "aria-expanded", "true");
188
+ setAttr(element, "aria-controls", listId);
189
+ setActive(Math.max(options.findIndex((o) => o.value === value), 0));
190
+ };
191
+ const search = (char) => {
192
+ const now = Date.now();
193
+ typed = now - typedAt > TYPEAHEAD_MS ? char : typed + char;
194
+ typedAt = now;
195
+ const find = (text) => options.findIndex((o) => !o.disabled && o.label.toLocaleLowerCase().startsWith(text.toLocaleLowerCase()));
196
+ let index = find(typed);
197
+ if (index < 0 && typed.length > 1) {
198
+ typed = char;
199
+ index = find(char);
200
+ }
201
+ return index;
202
+ };
203
+ const searching = () => typed !== "" && Date.now() - typedAt <= TYPEAHEAD_MS;
204
+ element.addEventListener("click", () => popover ? close() : open());
205
+ element.addEventListener("keydown", (event) => {
206
+ const opened = popover !== null;
207
+ let handled = true;
208
+ const key = event.key === " " && opened && searching() ? "Char" : event.key;
209
+ switch (key) {
210
+ case "ArrowDown":
211
+ if (opened) setActive(active + 1);
212
+ else open();
213
+ break;
214
+ case "ArrowUp":
215
+ if (opened && event.altKey) choose(active);
216
+ else if (opened) setActive(active - 1, -1);
217
+ else open();
218
+ break;
219
+ case "PageDown":
220
+ if (opened) setActive(active + PAGE);
221
+ else handled = false;
222
+ break;
223
+ case "PageUp":
224
+ if (opened) setActive(active - PAGE, -1);
225
+ else handled = false;
226
+ break;
227
+ case "Home":
228
+ if (opened) setActive(0);
229
+ else handled = false;
230
+ break;
231
+ case "End":
232
+ if (opened) setActive(optionEls.length - 1, -1);
233
+ else handled = false;
234
+ break;
235
+ case "Enter":
236
+ case " ":
237
+ if (opened) choose(active);
238
+ else open();
239
+ break;
240
+ case "Escape":
241
+ if (opened) close();
242
+ else handled = false;
243
+ break;
244
+ case "Tab":
245
+ close();
246
+ handled = false;
247
+ break;
248
+ default:
249
+ handled = false;
250
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
251
+ const index = search(event.key);
252
+ if (index >= 0 && opened) setActive(index);
253
+ else if (index >= 0 && options[index].value !== value) {
254
+ value = options[index].value;
255
+ render();
256
+ init.onChange(value);
257
+ }
258
+ handled = true;
259
+ }
260
+ }
261
+ if (handled) {
262
+ event.preventDefault();
263
+ event.stopPropagation();
264
+ }
265
+ });
266
+ render();
267
+ return {
268
+ element,
269
+ get value() {
270
+ return value;
271
+ },
272
+ setValue(next) {
273
+ value = next;
274
+ render();
275
+ },
276
+ setOptions(next) {
277
+ options = next;
278
+ close();
279
+ render();
280
+ },
281
+ setLabel(label2) {
282
+ setAttr(element, "aria-label", label2);
283
+ },
284
+ focus: () => element.focus(),
285
+ destroy() {
286
+ close();
287
+ element.remove();
288
+ }
289
+ };
290
+ }
291
+
292
+ // src/i18n.ts
293
+ var cs = {
294
+ chooseDate: "Vybrat datum",
295
+ chooseDateTime: "Vybrat datum a \u010Das",
296
+ calendar: "Kalend\xE1\u0159",
297
+ previousMonth: "P\u0159edchoz\xED m\u011Bs\xEDc",
298
+ nextMonth: "Dal\u0161\xED m\u011Bs\xEDc",
299
+ today: "Dnes",
300
+ clear: "Vymazat",
301
+ time: "\u010Cas",
302
+ invalidDate: "Neplatn\xE9 datum"
303
+ };
304
+ var en = {
305
+ chooseDate: "Choose date",
306
+ chooseDateTime: "Choose date and time",
307
+ calendar: "Calendar",
308
+ previousMonth: "Previous month",
309
+ nextMonth: "Next month",
310
+ today: "Today",
311
+ clear: "Clear",
312
+ time: "Time",
313
+ invalidDate: "Invalid date"
314
+ };
315
+ var CONTROLS_MESSAGES = { cs, en };
316
+ function resolveControlsMessages(locale, overrides = {}) {
317
+ return { ...locale.toLowerCase().startsWith("en") ? en : cs, ...overrides };
318
+ }
319
+
320
+ // src/date-text.ts
321
+ var ISO = /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2}))?$/;
322
+ var LOCAL = /^(\d{1,4})\s*([./-])\s*(\d{1,4})\s*\2\s*(\d{1,4})\.?(?:\s+(\d{1,2}):(\d{2}))?$/;
323
+ var pad = (n) => String(n).padStart(2, "0");
324
+ function isValidDay(year, month, day) {
325
+ const date = new Date(Date.UTC(year, month - 1, day));
326
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
327
+ }
328
+ var toIsoDay = (d) => `${d.year}-${pad(d.month)}-${pad(d.day)}`;
329
+ function dayOf(value, kind) {
330
+ if (!value) return null;
331
+ if (kind === "date") {
332
+ const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
333
+ return match ? { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) } : null;
334
+ }
335
+ const date = new Date(value);
336
+ return Number.isNaN(date.getTime()) ? null : { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() };
337
+ }
338
+ function timeOf(value) {
339
+ const date = value ? new Date(value) : null;
340
+ return date && !Number.isNaN(date.getTime()) ? `${pad(date.getHours())}:${pad(date.getMinutes())}` : "00:00";
341
+ }
342
+ function parseTime(text) {
343
+ const match = /^\s*(\d{1,2}):(\d{2})\s*$/.exec(text);
344
+ if (!match) return void 0;
345
+ const hours = Number(match[1]);
346
+ const minutes = Number(match[2]);
347
+ return hours < 24 && minutes < 60 ? { hours, minutes } : void 0;
348
+ }
349
+ function valueOf(day, kind, time = { hours: 0, minutes: 0 }) {
350
+ return kind === "date" ? toIsoDay(day) : new Date(day.year, day.month - 1, day.day, time.hours, time.minutes).toISOString();
351
+ }
352
+ function dateOrder(locale) {
353
+ const parts = new Intl.DateTimeFormat(locale, { day: "numeric", month: "numeric", year: "numeric", timeZone: "UTC" }).formatToParts(
354
+ new Date(Date.UTC(2026, 1, 1))
355
+ );
356
+ return parts.filter((p) => p.type === "day" || p.type === "month" || p.type === "year").map((p) => p.type);
357
+ }
358
+ function fullYear(twoDigits) {
359
+ const year = 2e3 + twoDigits;
360
+ return year <= (/* @__PURE__ */ new Date()).getFullYear() + 20 ? year : year - 100;
361
+ }
362
+ function parseDateText(text, locale, kind) {
363
+ const trimmed = text.trim();
364
+ if (trimmed === "") return null;
365
+ let year;
366
+ let month;
367
+ let day;
368
+ let hours;
369
+ let minutes;
370
+ const iso = ISO.exec(trimmed);
371
+ if (iso) {
372
+ [year, month, day] = [Number(iso[1]), Number(iso[2]), Number(iso[3])];
373
+ [hours, minutes] = [iso[4], iso[5]];
374
+ } else {
375
+ const local = LOCAL.exec(trimmed);
376
+ if (!local) return void 0;
377
+ const numbers = [local[1], local[3], local[4]];
378
+ const fields = {};
379
+ dateOrder(locale).forEach((field, i) => fields[field] = numbers[i] ?? "");
380
+ const rawYear = fields.year ?? "";
381
+ if ((fields.day ?? "").length > 2 || (fields.month ?? "").length > 2 || rawYear.length !== 2 && rawYear.length !== 4) return void 0;
382
+ [day, month] = [Number(fields.day), Number(fields.month)];
383
+ year = rawYear.length === 2 ? fullYear(Number(rawYear)) : Number(rawYear);
384
+ [hours, minutes] = [local[5], local[6]];
385
+ }
386
+ if (!isValidDay(year, month, day)) return void 0;
387
+ if (kind === "date") return hours === void 0 ? toIsoDay({ year, month, day }) : void 0;
388
+ const time = hours === void 0 ? { hours: 0, minutes: 0 } : parseTime(`${hours}:${minutes}`);
389
+ return time ? valueOf({ year, month, day }, kind, time) : void 0;
390
+ }
391
+ function formatDateText(value, locale, kind) {
392
+ const day = dayOf(value, kind);
393
+ if (!day) return "";
394
+ const text = new Intl.DateTimeFormat(locale, { day: "numeric", month: "numeric", year: "numeric", timeZone: "UTC" }).format(
395
+ new Date(Date.UTC(day.year, day.month - 1, day.day))
396
+ );
397
+ return kind === "date" ? text : `${text} ${timeOf(value)}`;
398
+ }
399
+ function datePattern(locale, kind) {
400
+ const czech = /^(cs|sk)\b/i.test(locale);
401
+ const letters = { day: "d", month: "m", year: czech ? "rrrr" : "yyyy" };
402
+ const date = new Intl.DateTimeFormat(locale, { day: "numeric", month: "numeric", year: "numeric", timeZone: "UTC" }).formatToParts(new Date(Date.UTC(2026, 1, 1))).map((part) => letters[part.type] ?? part.value).join("").replace(/[\u00a0\u202f]/g, " ");
403
+ return kind === "date" ? date : `${date} hh:mm`;
404
+ }
405
+
406
+ // src/calendar.ts
407
+ var sameDay = (a, b) => a !== null && a.year === b.year && a.month === b.month && a.day === b.day;
408
+ function addDays(day, days) {
409
+ const date = new Date(Date.UTC(day.year, day.month - 1, day.day + days));
410
+ return { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1, day: date.getUTCDate() };
411
+ }
412
+ function addMonths(day, months) {
413
+ const first = new Date(Date.UTC(day.year, day.month - 1 + months, 1));
414
+ const year = first.getUTCFullYear();
415
+ const month = first.getUTCMonth() + 1;
416
+ const last = new Date(Date.UTC(year, month, 0)).getUTCDate();
417
+ return { year, month, day: Math.min(day.day, last) };
418
+ }
419
+ function firstWeekday(locale) {
420
+ try {
421
+ const intl = new Intl.Locale(locale);
422
+ const firstDay = (intl.getWeekInfo?.() ?? intl.weekInfo)?.firstDay;
423
+ if (firstDay !== void 0) return firstDay % 7;
424
+ } catch {
425
+ }
426
+ return /^en(-US)?$/i.test(locale) ? 0 : 1;
427
+ }
428
+ function createCalendar(options) {
429
+ const m = options.messages;
430
+ let focused = options.selected ?? options.today;
431
+ let time = options.time;
432
+ const titleId = uniqueId("rs-calendar-title");
433
+ const title = h("span", { class: "rs-calendar__title", attrs: { id: titleId, "aria-live": "polite" } });
434
+ const nav = (step, label, glyph) => {
435
+ const button = h("button", { class: "rs-calendar__nav", attrs: { type: "button", "aria-label": label, "data-step": step }, text: glyph });
436
+ button.addEventListener("click", () => {
437
+ focused = addMonths(focused, step);
438
+ render();
439
+ });
440
+ return button;
441
+ };
442
+ const weekdays = h("div", { class: "rs-calendar__weekdays", attrs: { role: "row" } });
443
+ const grid = h("div", { class: "rs-calendar__grid", attrs: { role: "grid", "aria-labelledby": titleId } });
444
+ const element = h(
445
+ "div",
446
+ { class: "rs-calendar", attrs: { role: "dialog", "aria-modal": "false", "aria-label": `${m.calendar}: ${options.label}` } },
447
+ h("div", { class: "rs-calendar__head" }, title, nav(-1, m.previousMonth, "\u2039"), nav(1, m.nextMonth, "\u203A")),
448
+ grid
449
+ );
450
+ const start = firstWeekday(options.locale);
451
+ const weekdayFormat = new Intl.DateTimeFormat(options.locale, { weekday: "short", timeZone: "UTC" });
452
+ const weekdayName = new Intl.DateTimeFormat(options.locale, { weekday: "long", timeZone: "UTC" });
453
+ for (let i = 0; i < 7; i++) {
454
+ const date = new Date(Date.UTC(2026, 0, 4 + start + i));
455
+ weekdays.append(h("span", { attrs: { role: "columnheader", "aria-label": weekdayName.format(date) }, text: weekdayFormat.format(date) }));
456
+ }
457
+ const monthFormat = new Intl.DateTimeFormat(options.locale, { month: "long", year: "numeric", timeZone: "UTC" });
458
+ const fullFormat = new Intl.DateTimeFormat(options.locale, { dateStyle: "full", timeZone: "UTC" });
459
+ let timeInput = null;
460
+ const pick = (day) => {
461
+ const parsed = options.kind === "datetime" ? parseTime(time) : { hours: 0, minutes: 0 };
462
+ if (!parsed) {
463
+ if (timeInput) {
464
+ setAttr(timeInput, "aria-invalid", "true");
465
+ timeInput.focus();
466
+ }
467
+ return;
468
+ }
469
+ options.onPick(day, parsed);
470
+ };
471
+ const dayButtons = /* @__PURE__ */ new Map();
472
+ const keyOf = (d) => `${d.year}-${d.month}-${d.day}`;
473
+ const render = () => {
474
+ title.textContent = monthFormat.format(new Date(Date.UTC(focused.year, focused.month - 1, 1)));
475
+ const first = { year: focused.year, month: focused.month, day: 1 };
476
+ const weekday = new Date(Date.UTC(first.year, first.month - 1, 1)).getUTCDay();
477
+ let cursor = addDays(first, -((weekday - start + 7) % 7));
478
+ dayButtons.clear();
479
+ const rows = [];
480
+ for (let week = 0; week < 6; week++) {
481
+ const row = h("div", { attrs: { role: "row" } });
482
+ for (let i = 0; i < 7; i++) {
483
+ const day = cursor;
484
+ const isFocused = sameDay(focused, day);
485
+ const button = h("button", {
486
+ class: "rs-calendar__day",
487
+ attrs: {
488
+ type: "button",
489
+ role: "gridcell",
490
+ tabindex: isFocused ? 0 : -1,
491
+ "aria-label": fullFormat.format(new Date(Date.UTC(day.year, day.month - 1, day.day))),
492
+ "aria-selected": String(sameDay(options.selected, day)),
493
+ "data-selected": sameDay(options.selected, day),
494
+ "data-today": sameDay(options.today, day),
495
+ "data-outside": day.month !== focused.month
496
+ },
497
+ text: String(day.day)
498
+ });
499
+ button.addEventListener("click", () => pick(day));
500
+ dayButtons.set(keyOf(day), button);
501
+ row.append(button);
502
+ cursor = addDays(cursor, 1);
503
+ }
504
+ rows.push(row);
505
+ }
506
+ grid.replaceChildren(weekdays, ...rows);
507
+ };
508
+ const moveFocus = (next) => {
509
+ focused = next;
510
+ render();
511
+ dayButtons.get(keyOf(focused))?.focus();
512
+ };
513
+ grid.addEventListener("keydown", (event) => {
514
+ const weekday = (new Date(Date.UTC(focused.year, focused.month - 1, focused.day)).getUTCDay() - start + 7) % 7;
515
+ let next = null;
516
+ switch (event.key) {
517
+ case "ArrowLeft":
518
+ next = addDays(focused, -1);
519
+ break;
520
+ case "ArrowRight":
521
+ next = addDays(focused, 1);
522
+ break;
523
+ case "ArrowUp":
524
+ next = addDays(focused, -7);
525
+ break;
526
+ case "ArrowDown":
527
+ next = addDays(focused, 7);
528
+ break;
529
+ case "PageUp":
530
+ next = addMonths(focused, event.shiftKey ? -12 : -1);
531
+ break;
532
+ case "PageDown":
533
+ next = addMonths(focused, event.shiftKey ? 12 : 1);
534
+ break;
535
+ case "Home":
536
+ next = addDays(focused, -weekday);
537
+ break;
538
+ case "End":
539
+ next = addDays(focused, 6 - weekday);
540
+ break;
541
+ case "Enter":
542
+ case " ":
543
+ event.preventDefault();
544
+ event.stopPropagation();
545
+ pick(focused);
546
+ return;
547
+ default:
548
+ return;
549
+ }
550
+ event.preventDefault();
551
+ event.stopPropagation();
552
+ moveFocus(next);
553
+ });
554
+ element.addEventListener("keydown", (event) => {
555
+ if (event.key !== "Escape") return;
556
+ event.preventDefault();
557
+ event.stopPropagation();
558
+ options.onCancel();
559
+ });
560
+ const action = (name, label, run) => {
561
+ const button = h("button", { class: "rs-calendar__action", attrs: { type: "button", "data-action": name }, text: label });
562
+ button.addEventListener("click", run);
563
+ return button;
564
+ };
565
+ const foot = h("div", { class: "rs-calendar__foot" }, action("clear", m.clear, () => options.onClear()));
566
+ if (options.kind === "datetime") {
567
+ const field = h("input", { class: "rs-calendar__time", attrs: { type: "text", inputmode: "numeric", "aria-label": m.time, placeholder: "hh:mm" } });
568
+ field.value = time;
569
+ field.addEventListener("input", () => {
570
+ time = field.value;
571
+ setAttr(field, "aria-invalid", parseTime(time) ? null : "true");
572
+ });
573
+ field.addEventListener("keydown", (event) => {
574
+ if (event.key !== "Enter") return;
575
+ event.preventDefault();
576
+ event.stopPropagation();
577
+ pick(focused);
578
+ });
579
+ timeInput = field;
580
+ foot.append(field);
581
+ }
582
+ foot.append(action("today", m.today, () => pick(options.today)));
583
+ element.append(foot);
584
+ render();
585
+ return {
586
+ element,
587
+ focus: () => dayButtons.get(keyOf(focused))?.focus()
588
+ };
589
+ }
590
+
591
+ // src/date-input.ts
592
+ var CALENDAR_ICON = "M7 2v2H5a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-2V2h-2v2H9V2zM5 9h14v10H5z";
593
+ function calendarIcon() {
594
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
595
+ svg.setAttribute("viewBox", "0 0 24 24");
596
+ svg.setAttribute("width", "14");
597
+ svg.setAttribute("height", "14");
598
+ svg.setAttribute("aria-hidden", "true");
599
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
600
+ path.setAttribute("d", CALENDAR_ICON);
601
+ path.setAttribute("fill", "currentColor");
602
+ svg.append(path);
603
+ return svg;
604
+ }
605
+ function sameMinute(a, b) {
606
+ const x = new Date(a);
607
+ const y = new Date(b);
608
+ return Math.floor(x.getTime() / 6e4) === Math.floor(y.getTime() / 6e4);
609
+ }
610
+ var todayOf = () => {
611
+ const now = /* @__PURE__ */ new Date();
612
+ return { year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate() };
613
+ };
614
+ function createDateInput(options) {
615
+ const m = resolveControlsMessages(options.locale, options.messages);
616
+ let value = options.value;
617
+ let popover = null;
618
+ const input = h("input", {
619
+ class: "rs-date__input",
620
+ attrs: {
621
+ type: "text",
622
+ autocomplete: "off",
623
+ "aria-label": options.label,
624
+ "aria-keyshortcuts": "Alt+ArrowDown",
625
+ placeholder: datePattern(options.locale, options.kind)
626
+ }
627
+ });
628
+ const button = h(
629
+ "button",
630
+ {
631
+ class: "rs-date__button",
632
+ attrs: { type: "button", tabindex: -1, "aria-haspopup": "dialog", "aria-expanded": "false", "aria-label": options.kind === "date" ? m.chooseDate : m.chooseDateTime }
633
+ },
634
+ calendarIcon()
635
+ );
636
+ const element = h("div", { class: options.className ? `rs-date ${options.className}` : "rs-date" }, input, button);
637
+ let shown = "";
638
+ const show = () => {
639
+ shown = formatDateText(value, options.locale, options.kind);
640
+ input.value = shown;
641
+ setAttr(input, "aria-invalid", null);
642
+ setAttr(input, "title", null);
643
+ };
644
+ const set = (next) => {
645
+ const changed = next !== value;
646
+ value = next;
647
+ show();
648
+ if (changed) options.onChange(value);
649
+ };
650
+ const commitText = () => {
651
+ if (input.value === shown) {
652
+ setAttr(input, "aria-invalid", null);
653
+ return true;
654
+ }
655
+ const parsed = parseDateText(input.value, options.locale, options.kind);
656
+ if (parsed === void 0) {
657
+ setAttr(input, "aria-invalid", "true");
658
+ setAttr(input, "title", m.invalidDate);
659
+ return false;
660
+ }
661
+ setAttr(input, "title", null);
662
+ if (options.kind === "datetime" && parsed !== null && value !== null && sameMinute(parsed, value)) {
663
+ show();
664
+ return true;
665
+ }
666
+ set(parsed);
667
+ return true;
668
+ };
669
+ const close = () => popover?.close();
670
+ const open = () => {
671
+ if (popover) return;
672
+ const typed = parseDateText(input.value, options.locale, options.kind);
673
+ const shown2 = typed === void 0 ? value : typed;
674
+ const calendar = createCalendar({
675
+ kind: options.kind,
676
+ selected: dayOf(shown2, options.kind),
677
+ today: todayOf(),
678
+ time: timeOf(options.kind === "datetime" ? shown2 : null),
679
+ locale: options.locale,
680
+ messages: m,
681
+ label: options.label,
682
+ onPick: (day, time) => {
683
+ close();
684
+ set(valueOf(day, options.kind, time));
685
+ input.focus();
686
+ },
687
+ onClear: () => {
688
+ close();
689
+ set(null);
690
+ input.focus();
691
+ },
692
+ onCancel: () => {
693
+ close();
694
+ input.focus();
695
+ }
696
+ });
697
+ popover = openControlPopover(element, calendar.element, {
698
+ onClose: () => {
699
+ popover = null;
700
+ setAttr(button, "aria-expanded", "false");
701
+ }
702
+ });
703
+ setAttr(button, "aria-expanded", "true");
704
+ calendar.focus();
705
+ };
706
+ input.addEventListener("blur", () => {
707
+ commitText();
708
+ });
709
+ input.addEventListener("keydown", (event) => {
710
+ if (event.key === "Enter") {
711
+ if (!commitText()) {
712
+ event.preventDefault();
713
+ event.stopPropagation();
714
+ }
715
+ } else if (event.key === "ArrowDown" && event.altKey) {
716
+ event.preventDefault();
717
+ event.stopPropagation();
718
+ open();
719
+ }
720
+ });
721
+ button.addEventListener("pointerdown", (event) => event.preventDefault());
722
+ button.addEventListener("click", () => popover ? close() : open());
723
+ show();
724
+ return {
725
+ element,
726
+ input,
727
+ get value() {
728
+ return value;
729
+ },
730
+ setValue(next) {
731
+ value = next;
732
+ show();
733
+ },
734
+ focus: () => input.focus(),
735
+ destroy() {
736
+ close();
737
+ element.remove();
738
+ }
739
+ };
740
+ }
741
+ export {
742
+ CONTROLS_MESSAGES,
743
+ POPOVER_OPEN_EVENT,
744
+ closeControlPopover,
745
+ createDateInput,
746
+ createSelect,
747
+ datePattern,
748
+ formatDateText,
749
+ openControlPopover,
750
+ parseDateText,
751
+ resolveControlsMessages
752
+ };
753
+ //# sourceMappingURL=index.js.map