@lekoala/date-picker 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- /*** @lekoala/date-picker v0.2.0 - https://github.com/lekoala/date-picker ***/
1
+ /*** @lekoala/date-picker v0.4.0 - https://github.com/lekoala/date-picker ***/
2
2
  (() => {
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -176,15 +176,46 @@
176
176
  const end = endOfWeek(endOfMonth(value), firstDay);
177
177
  const weeks = [];
178
178
  let cursor = start;
179
- while (compareDates(cursor, end) <= 0) {
179
+ for (;; ) {
180
180
  const week = [];
181
181
  for (let i = 0;i < 7; i++)
182
182
  week.push(addDays(cursor, i));
183
183
  weeks.push(week);
184
+ if (compareDates(week[6], end) >= 0)
185
+ break;
184
186
  cursor = addDays(cursor, 7);
185
187
  }
186
188
  return weeks;
187
189
  }
190
+ function dayOrdinal(year, month, day) {
191
+ const base = toUTCDate({ year: 1, month: 1, day: 1 });
192
+ return Math.round((toUTCDate({ year, month, day }).getTime() - base.getTime()) / 86400000);
193
+ }
194
+ function clampDisplayMonth(value, firstDay = 1) {
195
+ const normalized = normalizeFirstDay(firstDay);
196
+ const maxOrdinal = dayOrdinal(9999, 12, 31);
197
+ let key = monthKey(value);
198
+ for (;; ) {
199
+ const parsed = parseDate(`${key}-01`);
200
+ if (!parsed)
201
+ throw new TypeError(`Invalid civil date/month: ${value}`);
202
+ const startOrdinal = dayOrdinal(parsed.year, parsed.month, 1);
203
+ const startOffset = (toUTCDate(parsed).getUTCDay() - normalized + 7) % 7;
204
+ if (startOrdinal - startOffset < 0) {
205
+ key = shiftMonth(key, 1);
206
+ continue;
207
+ }
208
+ const lastDay = daysInMonth(parsed.year, parsed.month);
209
+ const endParts = { year: parsed.year, month: parsed.month, day: lastDay };
210
+ const endOrdinal = startOrdinal + lastDay - 1;
211
+ const endOffset = (toUTCDate(endParts).getUTCDay() - normalized + 7) % 7;
212
+ if (endOrdinal - endOffset + 6 > maxOrdinal) {
213
+ key = shiftMonth(key, -1);
214
+ continue;
215
+ }
216
+ return key;
217
+ }
218
+ }
188
219
  function clampDate(value, min = "", max = "") {
189
220
  if (!isDate(value))
190
221
  throw new TypeError(`Invalid civil date: ${value}`);
@@ -312,36 +343,140 @@
312
343
  focus(endpoint) {
313
344
  this.activeEndpoint = endpoint;
314
345
  }
315
- activate(date, editable = () => true) {
346
+ _refuse(endpoint) {
347
+ return {
348
+ status: "refused",
349
+ endpoint,
350
+ changedEndpoints: [],
351
+ range: this.range,
352
+ activeEndpoint: this.activeEndpoint
353
+ };
354
+ }
355
+ project(date, editable = () => true) {
316
356
  if (!isDate(date))
317
357
  throw new TypeError(`Invalid range activation: ${date}`);
318
- if (this.complete && (compareDates(date, this.start) < 0 || compareDates(date, this.end) > 0)) {
319
- const endpoint = compareDates(date, this.start) < 0 ? "start" : "end";
358
+ const { start, end } = this;
359
+ if (this.complete && (compareDates(date, start) < 0 || compareDates(date, end) > 0)) {
360
+ const endpoint = compareDates(date, start) < 0 ? "start" : "end";
320
361
  if (!editable(endpoint))
321
- return { status: "refused", endpoint };
322
- this[endpoint] = date;
323
- this.activeEndpoint = endpoint;
324
- return { status: "complete", endpoint };
362
+ return this._refuse(endpoint);
363
+ return {
364
+ status: "complete",
365
+ endpoint,
366
+ changedEndpoints: [endpoint],
367
+ range: endpoint === "start" ? { start: date, end } : { start, end: date },
368
+ activeEndpoint: endpoint
369
+ };
325
370
  }
326
371
  if (this.activeEndpoint === "end") {
327
- if (this.start && compareDates(date, this.start) < 0) {
328
- return { status: "refused", endpoint: "end" };
372
+ if (start && !end && compareDates(date, start) < 0) {
373
+ if (!editable("start"))
374
+ return this._refuse("start");
375
+ if (!editable("end"))
376
+ return this._refuse("end");
377
+ return {
378
+ status: "complete",
379
+ endpoint: "end",
380
+ changedEndpoints: ["start", "end"],
381
+ range: { start: date, end: start },
382
+ activeEndpoint: "end"
383
+ };
329
384
  }
385
+ if (start && compareDates(date, start) < 0)
386
+ return this._refuse("end");
330
387
  if (!editable("end"))
331
- return { status: "refused", endpoint: "end" };
332
- this.end = date;
333
- return { status: this.start ? "complete" : "pending", endpoint: "end" };
388
+ return this._refuse("end");
389
+ return {
390
+ status: start ? "complete" : "pending",
391
+ endpoint: "end",
392
+ changedEndpoints: ["end"],
393
+ range: { start, end: date },
394
+ activeEndpoint: "end"
395
+ };
334
396
  }
335
- if (!editable("start"))
336
- return { status: "refused", endpoint: "start" };
337
- this.start = date;
338
- if (!editable("end")) {
339
- return { status: "pending", endpoint: "start", close: true };
397
+ if (end && !start && compareDates(date, end) > 0) {
398
+ if (!editable("start"))
399
+ return this._refuse("start");
400
+ if (!editable("end"))
401
+ return this._refuse("end");
402
+ return {
403
+ status: "complete",
404
+ endpoint: "start",
405
+ changedEndpoints: ["start", "end"],
406
+ range: { start: end, end: date },
407
+ activeEndpoint: "start"
408
+ };
340
409
  }
341
- this.activeEndpoint = "end";
342
- return { status: "pending", endpoint: "start" };
410
+ if (!editable("start"))
411
+ return this._refuse("start");
412
+ const canContinue = editable("end");
413
+ return {
414
+ status: "pending",
415
+ endpoint: "start",
416
+ changedEndpoints: ["start"],
417
+ range: { start: date, end },
418
+ activeEndpoint: canContinue ? "end" : this.activeEndpoint,
419
+ ...canContinue ? {} : { close: true }
420
+ };
421
+ }
422
+ projectEndpoint(date, endpoint, editable = () => true) {
423
+ if (!isDate(date))
424
+ throw new TypeError(`Invalid range activation: ${date}`);
425
+ if (!this.complete || !editable(endpoint))
426
+ return this._refuse(endpoint);
427
+ const { start, end } = this;
428
+ const previous = endpoint === "start" ? start : end;
429
+ const target = endpoint === "start" ? earlier(date, end) : later(date, start);
430
+ return {
431
+ status: "complete",
432
+ endpoint,
433
+ changedEndpoints: target === previous ? [] : [endpoint],
434
+ range: endpoint === "start" ? { start: target, end } : { start, end: target },
435
+ activeEndpoint: endpoint
436
+ };
437
+ }
438
+ previewRange(date, endpoint = "", editable = () => true) {
439
+ if (!isDate(date))
440
+ return null;
441
+ if (!endpoint && Boolean(this.start) === Boolean(this.end))
442
+ return null;
443
+ const transition = endpoint ? this.projectEndpoint(date, endpoint, editable) : this.project(date, editable);
444
+ if (transition.status === "refused")
445
+ return null;
446
+ const { start, end } = transition.range;
447
+ if (!start || end && compareDates(end, start) < 0)
448
+ return null;
449
+ return { start, end };
450
+ }
451
+ _apply(transition) {
452
+ if (transition.status === "refused")
453
+ return transition;
454
+ this.start = transition.range.start;
455
+ this.end = transition.range.end;
456
+ this.activeEndpoint = transition.activeEndpoint;
457
+ return transition;
458
+ }
459
+ activate(date, editable = () => true) {
460
+ return this._apply(this.project(date, editable));
461
+ }
462
+ moveEndpoint(date, endpoint, editable = () => true) {
463
+ return this._apply(this.projectEndpoint(date, endpoint, editable));
343
464
  }
344
465
  }
466
+ function later(a, b) {
467
+ if (!a)
468
+ return b;
469
+ if (!b)
470
+ return a;
471
+ return compareDates(a, b) >= 0 ? a : b;
472
+ }
473
+ function earlier(a, b) {
474
+ if (!a)
475
+ return b;
476
+ if (!b)
477
+ return a;
478
+ return compareDates(a, b) <= 0 ? a : b;
479
+ }
345
480
 
346
481
  // src/intl.js
347
482
  var BIDI = /[\u200e\u200f\u061c\u202a-\u202e\u2066-\u2069]/g;
@@ -361,8 +496,10 @@
361
496
  map.set(formatter.format(i), String(i));
362
497
  return map;
363
498
  }
364
- function normalizeDigits(text, locale) {
365
- let result = String(text || "").replace(BIDI, "").trim();
499
+ function normalizeDigits(text, locale, options = {}) {
500
+ let result = String(text || "").replace(BIDI, "");
501
+ if (options.trim !== false)
502
+ result = result.trim();
366
503
  for (const [local, ascii] of digitMap(locale))
367
504
  result = result.split(local).join(ascii);
368
505
  return result;
@@ -462,8 +599,8 @@
462
599
  return "(\\d{1,2})";
463
600
  if (part.type === "year")
464
601
  return "(\\d{4})";
465
- const literal = normalizeDigits(part.value, resolved).replace(/\s+/g, " ");
466
- return escapeRegExp(literal).replace(/\\ /g, "\\s*");
602
+ const literal = normalizeDigits(part.value, resolved, { trim: false });
603
+ return escapeRegExp(literal).replace(/\s+/g, "\\s*");
467
604
  }).join("");
468
605
  const regex = new RegExp(`^\\s*${pattern}\\s*$`);
469
606
  return {
@@ -557,19 +694,24 @@
557
694
  function yearOf(month) {
558
695
  return Number(month.slice(0, 4));
559
696
  }
697
+ function stepMonthKey(month, amount) {
698
+ const [year, index] = month.split("-").map(Number);
699
+ const absolute = year * 12 + (index - 1) + amount;
700
+ const stepYear = Math.floor(absolute / 12);
701
+ const stepMonth = (absolute % 12 + 12) % 12 + 1;
702
+ if (stepYear < 1 || stepYear > 9999)
703
+ return null;
704
+ return `${String(stepYear).padStart(4, "0")}-${String(stepMonth).padStart(2, "0")}`;
705
+ }
706
+ function rangeAttributeMap(position) {
707
+ return {
708
+ "data-range-start": position === "start" || position === "single",
709
+ "data-range-end": position === "end" || position === "single",
710
+ "data-in-range": position === "in"
711
+ };
712
+ }
560
713
  function rangeAttributes(position) {
561
- switch (position) {
562
- case "single":
563
- return ' data-range-start="true" data-range-end="true"';
564
- case "start":
565
- return ' data-range-start="true"';
566
- case "end":
567
- return ' data-range-end="true"';
568
- case "in":
569
- return ' data-in-range="true"';
570
- default:
571
- return "";
572
- }
714
+ return Object.entries(rangeAttributeMap(position)).filter(([, on]) => on).map(([name]) => ` ${name}="true"`).join("");
573
715
  }
574
716
  function rangeLabelKey(position) {
575
717
  if (position === "single")
@@ -612,6 +754,8 @@
612
754
  this._renderDay = null;
613
755
  this._isDateDisabled = null;
614
756
  this._highlightedRange = { start: "", end: "" };
757
+ this._paintedRange = { start: "", end: "" };
758
+ this._paintedPreview = false;
615
759
  this._messages = getDefaultMessages();
616
760
  this._onClick = this._onClick.bind(this);
617
761
  this._onChange = this._onChange.bind(this);
@@ -730,11 +874,12 @@
730
874
  }
731
875
  set highlightedRange(value) {
732
876
  const next = normalizeRange(value);
733
- if (next.start === this._highlightedRange.start && next.end === this._highlightedRange.end)
877
+ const preview = this.hasAttribute("data-range-preview");
878
+ if (next.start === this._paintedRange.start && next.end === this._paintedRange.end && preview === this._paintedPreview)
734
879
  return;
735
880
  this._highlightedRange = next;
736
881
  if (this._connected)
737
- this.render();
882
+ this._updateRangeDom();
738
883
  }
739
884
  get fixedWeeks() {
740
885
  return this.hasAttribute("fixed-weeks");
@@ -796,7 +941,7 @@
796
941
  const attrDisplay = this.getAttribute("display") || "";
797
942
  if (attrDisplay) {
798
943
  try {
799
- this._model.setDisplay(attrDisplay);
944
+ this._model.setDisplay(clampDisplayMonth(attrDisplay, firstDay));
800
945
  } catch {}
801
946
  } else if (this._model.value) {
802
947
  this._model.display = monthKey(this._model.value);
@@ -817,10 +962,18 @@
817
962
  this._rendering = false;
818
963
  }
819
964
  _clamp(value) {
820
- return clampDate(value, this.min, this.max);
965
+ const bounded = clampDate(value, this.min, this.max);
966
+ const month = monthKey(bounded);
967
+ const safeMonth = clampDisplayMonth(month, this.firstDay);
968
+ if (safeMonth === month)
969
+ return bounded;
970
+ const day = Number(bounded.slice(8, 10)) || 1;
971
+ const candidate = addDays(`${safeMonth}-01`, day - 1);
972
+ const last = endOfMonth(safeMonth);
973
+ return compareDates(candidate, last) > 0 ? last : candidate;
821
974
  }
822
975
  _weeks(display = this.display) {
823
- const weeks = getMonthWeeks(display, { firstDay: this.firstDay });
976
+ const weeks = getMonthWeeks(clampDisplayMonth(display, this.firstDay), { firstDay: this.firstDay });
824
977
  if (!this.fixedWeeks || weeks.length >= 6)
825
978
  return weeks;
826
979
  let cursor = weeks[weeks.length - 1]?.[6] || "";
@@ -834,7 +987,7 @@
834
987
  return weeks;
835
988
  }
836
989
  _focusTargetKey(element) {
837
- if (element.matches(".dp-month-select, .dp-year-select, .dp-year-input") && element.id) {
990
+ if (element.matches(".dp-month-select, .dp-year-input") && element.id) {
838
991
  return `#${CSS.escape(element.id)}`;
839
992
  }
840
993
  if (element.matches(".dp-nav[data-calendar-action]")) {
@@ -934,7 +1087,7 @@
934
1087
  }
935
1088
  _setDisplay(display, options = {}) {
936
1089
  const previous = this._model.display;
937
- this._model.setDisplay(display);
1090
+ this._model.setDisplay(clampDisplayMonth(display, this.firstDay));
938
1091
  this._model.focused = this._clamp(this._model.focused);
939
1092
  this._model.display = monthKey(this._model.focused);
940
1093
  this._reflect("display", this._model.display);
@@ -952,10 +1105,10 @@
952
1105
  }));
953
1106
  }
954
1107
  previousMonth() {
955
- return this._setDisplay(shiftMonth(this.display, -1));
1108
+ return this._setDisplay(stepMonthKey(this.display, -1) ?? this.display);
956
1109
  }
957
1110
  nextMonth() {
958
- return this._setDisplay(shiftMonth(this.display, 1));
1111
+ return this._setDisplay(stepMonthKey(this.display, 1) ?? this.display);
959
1112
  }
960
1113
  focusDate(date, options = {}) {
961
1114
  if (!isDate(date))
@@ -1007,12 +1160,39 @@
1007
1160
  }
1008
1161
  return false;
1009
1162
  }
1163
+ _dayLabel(date, state, position) {
1164
+ const rangeLabel = this.hasAttribute("data-range-preview") ? "" : rangeLabelKey(position);
1165
+ const description = typeof state.description === "string" ? state.description : "";
1166
+ return [
1167
+ formatLongDate(date, this.locale),
1168
+ description,
1169
+ rangeLabel ? this._messages[rangeLabel] : "",
1170
+ state.disabled ? this._messages.unavailable : ""
1171
+ ].filter(Boolean).join(". ");
1172
+ }
1173
+ _updateRangeDom() {
1174
+ this._paintedRange = { ...this._highlightedRange };
1175
+ this._paintedPreview = this.hasAttribute("data-range-preview");
1176
+ for (const cell of this.querySelectorAll(".dp-day[data-date]")) {
1177
+ if (!(cell instanceof HTMLTableCellElement))
1178
+ continue;
1179
+ const date = cell.dataset.date || "";
1180
+ const position = rangePosition(date, this._highlightedRange);
1181
+ for (const [name, on] of Object.entries(rangeAttributeMap(position))) {
1182
+ if (on)
1183
+ cell.setAttribute(name, "true");
1184
+ else
1185
+ cell.removeAttribute(name);
1186
+ }
1187
+ cell.setAttribute("aria-label", this._dayLabel(date, this.getDateState(date), position));
1188
+ }
1189
+ }
1010
1190
  focusGrid() {
1011
1191
  const target = this.querySelector(`.dp-day[data-date="${CSS.escape(this.focusedDate)}"]`);
1012
1192
  if (target instanceof HTMLElement)
1013
1193
  target.focus();
1014
1194
  }
1015
- async _activate(date) {
1195
+ async activateDate(date) {
1016
1196
  const confirmed = await this.ensureDate(date);
1017
1197
  const state = this.getDateState(date);
1018
1198
  if (!confirmed || state.disabled) {
@@ -1068,7 +1248,7 @@
1068
1248
  this._reflect("display", this._model.display);
1069
1249
  if (previousDisplay !== this.display)
1070
1250
  this._emitDisplayChange();
1071
- this._activate(date);
1251
+ this.activateDate(date);
1072
1252
  }
1073
1253
  _onChange(event) {
1074
1254
  const target = event.target;
@@ -1076,13 +1256,9 @@
1076
1256
  this._setDisplay(`${this.display.slice(0, 4)}-${target.value}`);
1077
1257
  return;
1078
1258
  }
1079
- if (target instanceof HTMLSelectElement && target.matches(".dp-year-select")) {
1080
- this._setDisplay(`${target.value}-${this.display.slice(5, 7)}`);
1081
- return;
1082
- }
1083
1259
  if (target instanceof HTMLInputElement && target.matches(".dp-year-input")) {
1084
1260
  const year = Number(target.value);
1085
- if (!Number.isInteger(year) || year < 1 || year > 9999) {
1261
+ if (!Number.isInteger(year) || year < 1 || year > 9998) {
1086
1262
  if (this._connected)
1087
1263
  this.render(false);
1088
1264
  return;
@@ -1095,8 +1271,10 @@
1095
1271
  if (!(target instanceof HTMLTableCellElement) || !target.matches(".dp-day[data-date]"))
1096
1272
  return;
1097
1273
  const date = target.dataset.date || "";
1098
- if (isDate(date))
1099
- this._model.focused = date;
1274
+ if (!isDate(date))
1275
+ return;
1276
+ this._model.focused = date;
1277
+ this.dispatchEvent(new CustomEvent("datefocus", { detail: { date, state: this.getDateState(date) }, bubbles: true }));
1100
1278
  }
1101
1279
  _onKeyDown(event) {
1102
1280
  const target = event.target;
@@ -1105,7 +1283,7 @@
1105
1283
  const date = target.dataset.date || this.focusedDate;
1106
1284
  if (event.key === "Enter" || event.key === " ") {
1107
1285
  event.preventDefault();
1108
- this._activate(date);
1286
+ this.activateDate(date);
1109
1287
  return;
1110
1288
  }
1111
1289
  const anchorFocused = this._model.focused;
@@ -1154,8 +1332,10 @@
1154
1332
  const gridId = `${this._id}-grid`;
1155
1333
  const minMonth = this.min ? monthKey(this.min) : "";
1156
1334
  const maxMonth = this.max ? monthKey(this.max) : "";
1157
- const prevDisabled = Boolean(minMonth && shiftMonth(display, -1) < minMonth);
1158
- const nextDisabled = Boolean(maxMonth && shiftMonth(display, 1) > maxMonth);
1335
+ const prevKey = stepMonthKey(display, -1);
1336
+ const nextKey = stepMonthKey(display, 1);
1337
+ const prevDisabled = Boolean(minMonth && prevKey !== null && prevKey < minMonth);
1338
+ const nextDisabled = Boolean(maxMonth && nextKey !== null && nextKey > maxMonth);
1159
1339
  const minYearAttr = this.min ? ` min="${Number(this.min.slice(0, 4))}"` : "";
1160
1340
  const maxYearAttr = this.max ? ` max="${Number(this.max.slice(0, 4))}"` : "";
1161
1341
  const showWeeks = this.showWeekNumbers && this.firstDay === 1;
@@ -1176,14 +1356,7 @@
1176
1356
  const focused = this.focusedDate === date;
1177
1357
  const isToday = date === today;
1178
1358
  const position = rangePosition(date, this._highlightedRange);
1179
- const rangeLabel = rangeLabelKey(position);
1180
- const description = typeof state.description === "string" ? state.description : "";
1181
- const label = [
1182
- formatLongDate(date, locale),
1183
- description,
1184
- rangeLabel ? this._messages[rangeLabel] : "",
1185
- state.disabled ? this._messages.unavailable : ""
1186
- ].filter(Boolean).join(". ");
1359
+ const label = this._dayLabel(date, state, position);
1187
1360
  return `<td class="dp-day" data-date="${date}"${outside ? ' data-outside-month="true"' : ""}${isToday ? ' data-today="true" aria-current="date"' : ""}${selected ? ' data-selected="true" aria-selected="true"' : ""}${rangeAttributes(position)}${state.disabled ? ' data-disabled="true" aria-disabled="true"' : ""} tabindex="${focused ? "0" : "-1"}" aria-label="${escapeHtml(label)}"><span class="dp-day-number" aria-hidden="true">${Number(date.slice(8, 10))}</span><span class="dp-day-extra" aria-hidden="true"></span></td>`;
1188
1361
  }).join("");
1189
1362
  return `<tr>${weekNumber}${cells}</tr>`;
@@ -1202,8 +1375,8 @@
1202
1375
  <div class="dp-calendar-header">
1203
1376
  ${yearFirst ? yearControl : monthControl}
1204
1377
  ${yearFirst ? monthControl : yearControl}
1205
- <button type="button" class="dp-nav dp-prev" data-calendar-action="previous" aria-label="${escapeHtml(this._messages.previousMonth)}"${prevDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m10 4-4 4 4 4"/></svg></button>
1206
- <button type="button" class="dp-nav dp-next" data-calendar-action="next" aria-label="${escapeHtml(this._messages.nextMonth)}"${nextDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 4 4 4-4 4"/></svg></button>
1378
+ <button type="button" class="dp-nav dp-prev" data-calendar-action="previous" aria-label="${escapeHtml(this._messages.previousMonth)}"${prevDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 10 4-4 4 4"/></svg></button>
1379
+ <button type="button" class="dp-nav dp-next" data-calendar-action="next" aria-label="${escapeHtml(this._messages.nextMonth)}"${nextDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 6 4 4 4-4"/></svg></button>
1207
1380
  </div>
1208
1381
  <h2 id="${headingId}" class="dp-calendar-heading dp-visually-hidden" aria-live="polite">${escapeHtml(formatMonthYear(`${display}-15`, locale))}</h2>
1209
1382
  <table id="${gridId}" class="dp-grid" role="grid" aria-labelledby="${headingId}">
@@ -1212,6 +1385,8 @@
1212
1385
  </table>
1213
1386
  </div>`;
1214
1387
  this._rendering = false;
1388
+ this._paintedRange = { ...this._highlightedRange };
1389
+ this._paintedPreview = this.hasAttribute("data-range-preview");
1215
1390
  if (focusKey) {
1216
1391
  queueMicrotask(() => {
1217
1392
  let selector = focusKey;
@@ -1560,8 +1735,11 @@
1560
1735
  const availableHeight = `${getAvailableHeight(referenceRect, side, boundary, distance, shiftPadding)}px`;
1561
1736
  const { style } = floating;
1562
1737
  const roomChanged = style.getPropertyValue("--available-height") !== availableHeight;
1563
- style.left = `${coords.x}px`;
1564
- style.top = `${coords.y}px`;
1738
+ const win = options.coordinateSpace === "document" ? reference.ownerDocument.defaultView : null;
1739
+ const originX = win ? win.scrollX : 0;
1740
+ const originY = win ? win.scrollY : 0;
1741
+ style.left = `${coords.x + originX}px`;
1742
+ style.top = `${coords.y + originY}px`;
1565
1743
  style.setProperty("--arrow-x", `${arrowX}%`);
1566
1744
  style.setProperty("--arrow-y", `${arrowY}%`);
1567
1745
  style.setProperty("--available-height", availableHeight);
@@ -1668,19 +1846,26 @@
1668
1846
  }
1669
1847
  handleInput() {
1670
1848
  const input = this.input;
1671
- this._dirty = true;
1849
+ const text = input.value.trim();
1850
+ const canonical = this.canonical ? this.adapter.format(this.canonical) : "";
1672
1851
  this.dirty();
1852
+ if (text === canonical) {
1853
+ this._dirty = false;
1854
+ if (this.hidden)
1855
+ this.hidden.value = this.canonical;
1856
+ input.setCustomValidity("");
1857
+ return;
1858
+ }
1859
+ this._dirty = true;
1673
1860
  if (this.hidden)
1674
1861
  this.hidden.value = "";
1675
- const text = input.value.trim();
1676
1862
  if (!text) {
1677
1863
  input.setCustomValidity("");
1678
1864
  return;
1679
1865
  }
1680
- const canonical = this.canonical ? this.adapter.format(this.canonical) : "";
1681
1866
  input.setCustomValidity(text === canonical ? "" : this.messages.invalidDate);
1682
1867
  }
1683
- async commit() {
1868
+ async commit(options = {}) {
1684
1869
  const input = this.input;
1685
1870
  const commitId = ++this._commitId;
1686
1871
  const text = input.value.trim();
@@ -1700,7 +1885,7 @@
1700
1885
  return { status: "invalid" };
1701
1886
  }
1702
1887
  if (this.confirm) {
1703
- const result = await this.confirm(parsed);
1888
+ const result = await this.confirm(parsed, options);
1704
1889
  if (commitId !== this._commitId)
1705
1890
  return { status: "stale" };
1706
1891
  if (!result?.ok) {
@@ -1772,6 +1957,7 @@
1772
1957
 
1773
1958
  // src/date-picker.js
1774
1959
  var uid2 = 0;
1960
+ var DRAG_THRESHOLD = 4;
1775
1961
 
1776
1962
  class DatePickerElement extends HTMLElement {
1777
1963
  static observedAttributes = ["value", "locale", "min", "max", "open-on-focus", "month-format"];
@@ -1791,7 +1977,11 @@
1791
1977
  this._lastFocusEndpoint = "";
1792
1978
  this._rangeCommitId = 0;
1793
1979
  this._pendingIntents = new Map;
1794
- this._button = null;
1980
+ this._previewDate = "";
1981
+ this._drag = null;
1982
+ this._suppressGridClick = false;
1983
+ this._buttons = [];
1984
+ this._returnFocus = null;
1795
1985
  this._panel = null;
1796
1986
  this._calendar = null;
1797
1987
  this._originalDescribedBy = "";
@@ -1807,6 +1997,7 @@
1807
1997
  this._dateState = null;
1808
1998
  this._renderDay = null;
1809
1999
  this._isDateDisabled = null;
2000
+ this._coordinateSpace = "viewport";
1810
2001
  }
1811
2002
  _rangeMode() {
1812
2003
  return this.hasAttribute("range") && Boolean(this._range);
@@ -1831,7 +2022,7 @@
1831
2022
  this._discoverTimeFields();
1832
2023
  this._field = new DateFieldController(input, { messages: this._messages, locale: this.locale });
1833
2024
  this._field.onAttributesChanged = () => this._syncInputState();
1834
- this._field.confirm = (date) => this._confirmDate(date);
2025
+ this._field.confirm = (date, options) => this._confirmDate(date, options);
1835
2026
  this._setupFormValue();
1836
2027
  this._build();
1837
2028
  this._syncInputState();
@@ -1895,10 +2086,10 @@
1895
2086
  ];
1896
2087
  for (const field of this._fields) {
1897
2088
  field.onAttributesChanged = () => this._syncRangeFields();
1898
- field.confirm = (date) => this._confirmDate(date);
2089
+ field.confirm = (date, options) => this._confirmDate(date, options);
1899
2090
  field.setupFormValue();
1900
2091
  }
1901
- this._build(endInput);
2092
+ this._build();
1902
2093
  this._setRangeAria();
1903
2094
  this._syncRangeFields();
1904
2095
  this._bindRange();
@@ -1938,10 +2129,12 @@
1938
2129
  this._teardownField(this._field);
1939
2130
  }
1940
2131
  this._field = null;
1941
- this._button?.remove();
2132
+ for (const { button } of this._buttons)
2133
+ button.remove();
2134
+ this._buttons = [];
2135
+ this._returnFocus = null;
1942
2136
  this._panel?.remove();
1943
2137
  this._formatHint?.remove();
1944
- this._button = null;
1945
2138
  this._panel = null;
1946
2139
  this._calendar = null;
1947
2140
  this._formatHint = null;
@@ -2105,6 +2298,12 @@
2105
2298
  if (this._connected)
2106
2299
  this.validate();
2107
2300
  }
2301
+ get coordinateSpace() {
2302
+ return this._coordinateSpace;
2303
+ }
2304
+ set coordinateSpace(value) {
2305
+ this._coordinateSpace = value === "document" ? "document" : "viewport";
2306
+ }
2108
2307
  _adapter() {
2109
2308
  return this._field?.adapter ?? this._fields?.[0]?.adapter ?? createDateAdapter(this.locale);
2110
2309
  }
@@ -2116,8 +2315,9 @@
2116
2315
  if (!input)
2117
2316
  return;
2118
2317
  this._field?.syncInputState();
2119
- if (this._button)
2120
- this._button.disabled = input.disabled || input.readOnly;
2318
+ const button = this._buttons[0]?.button;
2319
+ if (button)
2320
+ button.disabled = input.disabled || input.readOnly;
2121
2321
  if (this._open && (input.disabled || input.readOnly))
2122
2322
  this.hide(false);
2123
2323
  }
@@ -2126,10 +2326,13 @@
2126
2326
  return;
2127
2327
  for (const field of this._fields)
2128
2328
  field.syncInputState();
2329
+ for (const { button, endpoint } of this._buttons) {
2330
+ const index = endpoint === "end" ? 1 : 0;
2331
+ const field = this._fields[index];
2332
+ button.disabled = Boolean(field?.input.disabled || field?.input.readOnly);
2333
+ }
2334
+ this._syncDragAffordance();
2129
2335
  const active = this._range?.activeEndpoint === "end" ? 1 : 0;
2130
- const disabled = this._fields.every((field) => field.input.disabled || field.input.readOnly);
2131
- if (this._button)
2132
- this._button.disabled = disabled;
2133
2336
  if (this._open && (this._fields[active]?.input.disabled || this._fields[active]?.input.readOnly)) {
2134
2337
  this.hide(false);
2135
2338
  }
@@ -2147,19 +2350,37 @@
2147
2350
  input.setAttribute("aria-controls", this._panel.id);
2148
2351
  }
2149
2352
  }
2150
- _build(anchorInput) {
2151
- const input = anchorInput || this._input;
2152
- if (!input)
2153
- return;
2154
- const panelId = `${this._id}-panel`;
2155
- const hintId = `${this._id}-format`;
2353
+ _createTrigger(endpoint) {
2156
2354
  const button = document.createElement("button");
2157
2355
  button.type = "button";
2158
2356
  button.className = "dp-picker-button";
2159
2357
  button.setAttribute("aria-haspopup", "dialog");
2160
2358
  button.setAttribute("aria-expanded", "false");
2161
- button.setAttribute("aria-controls", panelId);
2359
+ button.setAttribute("aria-controls", `${this._id}-panel`);
2360
+ if (endpoint)
2361
+ button.dataset.endpoint = endpoint;
2162
2362
  button.innerHTML = '<span aria-hidden="true"><svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><rect x="1.75" y="3" width="12.5" height="11" rx="1.75"/><path d="M1.75 6.75h12.5"/><path d="M5.25 1.75v2.5M10.75 1.75v2.5"/></svg></span>';
2363
+ return button;
2364
+ }
2365
+ _build() {
2366
+ const ranges = this._rangeMode();
2367
+ const targets = [];
2368
+ if (ranges) {
2369
+ for (const [index, field] of (this._fields || []).entries()) {
2370
+ targets.push([field.input, index === 1 ? "end" : "start"]);
2371
+ }
2372
+ } else if (this._input) {
2373
+ targets.push([this._input, ""]);
2374
+ }
2375
+ if (!targets.length)
2376
+ return;
2377
+ const panelId = `${this._id}-panel`;
2378
+ const hintId = `${this._id}-format`;
2379
+ this._buttons = targets.map(([input, endpoint]) => {
2380
+ const button = this._createTrigger(endpoint);
2381
+ input.insertAdjacentElement("afterend", button);
2382
+ return { button, endpoint };
2383
+ });
2163
2384
  const panel = document.createElement("div");
2164
2385
  panel.id = panelId;
2165
2386
  panel.className = "dp-picker-panel";
@@ -2173,28 +2394,25 @@
2173
2394
  const hint = document.createElement("span");
2174
2395
  hint.id = hintId;
2175
2396
  hint.className = "dp-visually-hidden";
2176
- input.insertAdjacentElement("afterend", button);
2177
- button.insertAdjacentElement("afterend", panel);
2178
- panel.insertAdjacentElement("afterend", hint);
2179
- this._button = button;
2397
+ this.append(panel, hint);
2180
2398
  this._panel = panel;
2181
2399
  this._calendar = calendar;
2182
2400
  this._formatHint = hint;
2183
- if (!this._rangeMode()) {
2184
- this._originalDescribedBy = input.getAttribute("aria-describedby") || "";
2401
+ if (!ranges && this._input) {
2402
+ this._originalDescribedBy = this._input.getAttribute("aria-describedby") || "";
2185
2403
  const describedBy = [this._originalDescribedBy, hintId].filter(Boolean).join(" ");
2186
- input.setAttribute("aria-describedby", describedBy);
2187
- input.setAttribute("role", "combobox");
2188
- input.setAttribute("aria-haspopup", "dialog");
2189
- input.setAttribute("aria-expanded", "false");
2190
- input.setAttribute("aria-controls", panelId);
2404
+ this._input.setAttribute("aria-describedby", describedBy);
2405
+ this._input.setAttribute("role", "combobox");
2406
+ this._input.setAttribute("aria-haspopup", "dialog");
2407
+ this._input.setAttribute("aria-expanded", "false");
2408
+ this._input.setAttribute("aria-controls", panelId);
2191
2409
  }
2192
2410
  this._refreshLocale();
2193
2411
  this._refreshButtonLabel();
2194
2412
  }
2195
2413
  _bind() {
2196
2414
  const input = this._input;
2197
- const button = this._button;
2415
+ const button = this._buttons[0]?.button;
2198
2416
  const calendar = this._calendar;
2199
2417
  if (!input || !button || !calendar)
2200
2418
  return;
@@ -2234,8 +2452,9 @@
2234
2452
  this.hide(false);
2235
2453
  return;
2236
2454
  }
2237
- this.show();
2455
+ this.show({ returnFocus: button });
2238
2456
  }, { signal });
2457
+ button.addEventListener("keydown", (event) => this._onTriggerKeyDown("", event), { signal });
2239
2458
  calendar.addEventListener("dateactivate", (event) => {
2240
2459
  const custom = event;
2241
2460
  const date = custom.detail?.date;
@@ -2249,7 +2468,7 @@
2249
2468
  this._field?.dirty();
2250
2469
  this._setValue(date, { emit: true, format: true });
2251
2470
  this.hide(false);
2252
- this._focusInput();
2471
+ this._restoreFocus();
2253
2472
  });
2254
2473
  }, { signal });
2255
2474
  calendar.addEventListener("dateloadend", () => void this.validate(), { signal });
@@ -2271,9 +2490,8 @@
2271
2490
  this.addEventListener("keydown", (event) => this._onEscape(event), { signal });
2272
2491
  }
2273
2492
  _bindRange() {
2274
- const button = this._button;
2275
2493
  const calendar = this._calendar;
2276
- if (!this._fields || !button || !calendar)
2494
+ if (!this._fields || !this._buttons.length || !calendar)
2277
2495
  return;
2278
2496
  const controller = new AbortController;
2279
2497
  this._controller = controller;
@@ -2286,18 +2504,26 @@
2286
2504
  field.input.addEventListener("focus", (event) => this._onFieldFocus(endpoint, event), { signal });
2287
2505
  field.input.addEventListener("keydown", (event) => this._onFieldKeyDown(endpoint, event), { signal });
2288
2506
  }
2289
- button.addEventListener("click", (event) => {
2290
- if (this._open) {
2291
- if (event.detail === 0) {
2292
- setTimeout(() => this._calendar?.focusGrid(), 0);
2293
- return;
2294
- }
2295
- this.hide(false);
2507
+ for (const entry of this._buttons) {
2508
+ const endpoint = entry.endpoint;
2509
+ if (endpoint === "")
2510
+ continue;
2511
+ entry.button.addEventListener("click", (event) => this._onRangeTriggerClick(endpoint, event), {
2512
+ signal
2513
+ });
2514
+ entry.button.addEventListener("keydown", (event) => this._onTriggerKeyDown(endpoint, event), {
2515
+ signal
2516
+ });
2517
+ }
2518
+ calendar.addEventListener("click", (event) => {
2519
+ if (this._suppressGridClick) {
2520
+ this._suppressGridClick = false;
2521
+ event.preventDefault();
2522
+ event.stopImmediatePropagation();
2296
2523
  return;
2297
2524
  }
2298
- this.show();
2299
- }, { signal });
2300
- calendar.addEventListener("click", (event) => this._captureGridIntent(event), { capture: true, signal });
2525
+ this._captureGridIntent(event);
2526
+ }, { capture: true, signal });
2301
2527
  calendar.addEventListener("keydown", (event) => this._captureGridIntent(event), {
2302
2528
  capture: true,
2303
2529
  signal
@@ -2310,15 +2536,12 @@
2310
2536
  queueMicrotask(() => {
2311
2537
  if (custom.defaultPrevented || !this._connected || !this._range)
2312
2538
  return;
2313
- const pending = this._pendingIntents.get(date);
2314
- if (pending === undefined || pending !== this._rangeCommitId)
2539
+ const intent = this._pendingIntents.get(date);
2540
+ if (intent === undefined || intent.id !== this._rangeCommitId)
2315
2541
  return;
2316
2542
  this._pendingIntents.delete(date);
2317
- const result = this._range.activate(date, (which) => {
2318
- const index = which === "end" ? 1 : 0;
2319
- const field = this._fields?.[index];
2320
- return field != null && !field.input.disabled && !field.input.readOnly;
2321
- });
2543
+ const editable = (bound) => this._boundEditable(bound);
2544
+ const result = intent.endpoint ? this._range.moveEndpoint(date, intent.endpoint, editable) : this._range.activate(date, editable);
2322
2545
  if (result.status === "refused") {
2323
2546
  this._calendar?.dispatchEvent(new CustomEvent("dateinvalid", {
2324
2547
  detail: { date, state: this._calendar.getDateState(date) },
@@ -2326,13 +2549,23 @@
2326
2549
  }));
2327
2550
  return;
2328
2551
  }
2329
- this._setBound(result.endpoint, date, { emit: true, user: true });
2552
+ this._applyRangeTransition(result);
2553
+ if (intent.endpoint)
2554
+ return;
2330
2555
  if (result.close || result.status === "complete") {
2331
2556
  this.hide(false);
2332
2557
  this._focusField(result.endpoint);
2333
2558
  }
2334
2559
  });
2335
2560
  }, { signal });
2561
+ calendar.addEventListener("datefocus", (event) => this._onGridDateFocus(event), { signal });
2562
+ calendar.addEventListener("pointerover", (event) => this._onGridPointerOver(event), { signal });
2563
+ calendar.addEventListener("pointerleave", () => this._onGridPointerLeave(), { signal });
2564
+ calendar.addEventListener("pointerdown", (event) => this._onGridPointerDown(event), { signal });
2565
+ calendar.addEventListener("pointermove", (event) => this._onGridPointerMove(event), { signal });
2566
+ calendar.addEventListener("pointerup", (event) => this._onGridPointerUp(event), { signal });
2567
+ calendar.addEventListener("pointercancel", (event) => this._onGridPointerCancel(event), { signal });
2568
+ calendar.addEventListener("lostpointercapture", (event) => this._onGridPointerCancel(event), { signal });
2336
2569
  calendar.addEventListener("dateloadend", () => void this.validate(), { signal });
2337
2570
  this.ownerDocument.addEventListener("pointerdown", (event) => {
2338
2571
  if (!this._open || event.composedPath().includes(this))
@@ -2351,8 +2584,52 @@
2351
2584
  }, { capture: true, signal });
2352
2585
  this.addEventListener("keydown", (event) => this._onEscape(event), { signal });
2353
2586
  }
2587
+ _activateRangeEndpoint(endpoint) {
2588
+ const calendar = this._calendar;
2589
+ if (!this._range || !calendar)
2590
+ return;
2591
+ this._range.focus(endpoint);
2592
+ this._rangeCommitId++;
2593
+ this._pendingIntents.clear();
2594
+ this._lastFocusEndpoint = endpoint;
2595
+ const target = this._boundCanonical(endpoint) || todayISO();
2596
+ calendar.display = monthKey(target);
2597
+ calendar.focusedDate = target;
2598
+ this._clearPreview();
2599
+ }
2600
+ _onRangeTriggerClick(endpoint, event) {
2601
+ if (!this._open) {
2602
+ this.show({ endpoint });
2603
+ return;
2604
+ }
2605
+ if (event.detail === 0) {
2606
+ this._activateRangeEndpoint(endpoint);
2607
+ setTimeout(() => this._calendar?.focusGrid(), 0);
2608
+ return;
2609
+ }
2610
+ if (endpoint !== this._range?.activeEndpoint) {
2611
+ this._activateRangeEndpoint(endpoint);
2612
+ return;
2613
+ }
2614
+ this.hide(false);
2615
+ }
2616
+ _onTriggerKeyDown(endpoint, event) {
2617
+ if (event.key !== "ArrowDown")
2618
+ return;
2619
+ event.preventDefault();
2620
+ if (this._open) {
2621
+ if (this._rangeMode() && endpoint)
2622
+ this._activateRangeEndpoint(endpoint);
2623
+ setTimeout(() => this._calendar?.focusGrid(), 0);
2624
+ return;
2625
+ }
2626
+ if (this._rangeMode())
2627
+ this.show({ endpoint: endpoint || undefined });
2628
+ else
2629
+ this.show({ returnFocus: this._buttons[0]?.button });
2630
+ }
2354
2631
  _onFieldKeyDown(endpoint, event) {
2355
- if (event.key === "ArrowDown" || event.altKey && event.key === "ArrowDown") {
2632
+ if (event.key === "ArrowDown") {
2356
2633
  event.preventDefault();
2357
2634
  if (this._rangeMode() && endpoint) {
2358
2635
  this._lastFocusEndpoint = endpoint;
@@ -2400,7 +2677,191 @@
2400
2677
  const date = cell?.getAttribute("data-date") || "";
2401
2678
  if (!isDate(date))
2402
2679
  return;
2403
- this._pendingIntents.set(date, ++this._rangeCommitId);
2680
+ this._pendingIntents.clear();
2681
+ this._pendingIntents.set(date, { id: ++this._rangeCommitId, endpoint: "" });
2682
+ }
2683
+ _boundEditable(bound) {
2684
+ const field = this._fields?.[bound === "end" ? 1 : 0];
2685
+ return field != null && !field.input.disabled && !field.input.readOnly;
2686
+ }
2687
+ _applyRangeTransition(transition) {
2688
+ const fields = this._fields;
2689
+ if (!fields || !this._range || transition.status === "refused")
2690
+ return;
2691
+ const moved = [];
2692
+ for (const which of transition.changedEndpoints) {
2693
+ const field = fields[which === "end" ? 1 : 0];
2694
+ const next = which === "end" ? transition.range.end : transition.range.start;
2695
+ if (!field || field.canonical === next)
2696
+ continue;
2697
+ field.setCanonical(next, { format: true });
2698
+ moved.push(field);
2699
+ }
2700
+ this._syncHighlight();
2701
+ this._refreshRangeButtonLabel();
2702
+ this._revalidateRange(transition.endpoint);
2703
+ if (!moved.length)
2704
+ return;
2705
+ this._emitRangeChange();
2706
+ for (const field of moved) {
2707
+ field.input.dispatchEvent(new Event("input", { bubbles: true }));
2708
+ field.input.dispatchEvent(new Event("change", { bubbles: true }));
2709
+ }
2710
+ }
2711
+ _setPreview(date, endpoint = "") {
2712
+ const calendar = this._calendar;
2713
+ if (!calendar || !this._range || !this._open)
2714
+ return;
2715
+ const candidate = isDate(date) && !calendar.getDateState(date).disabled ? date : "";
2716
+ const preview = candidate ? this._range.previewRange(candidate, endpoint, (bound) => this._boundEditable(bound)) : null;
2717
+ if (!preview) {
2718
+ this._clearPreview();
2719
+ return;
2720
+ }
2721
+ this._previewDate = candidate;
2722
+ calendar.toggleAttribute("data-range-preview", true);
2723
+ calendar.highlightedRange = preview;
2724
+ }
2725
+ _clearPreview() {
2726
+ if (!this._previewDate)
2727
+ return;
2728
+ this._syncHighlight();
2729
+ }
2730
+ _onGridDateFocus(event) {
2731
+ if (this._drag)
2732
+ return;
2733
+ const date = event.detail?.date;
2734
+ if (isDate(date))
2735
+ this._setPreview(date);
2736
+ }
2737
+ _onGridPointerOver(event) {
2738
+ const pointer = event;
2739
+ if (this._drag || pointer.pointerType === "touch")
2740
+ return;
2741
+ this._setPreview(this._cellDate(pointer.target));
2742
+ }
2743
+ _onGridPointerLeave() {
2744
+ if (this._drag?.active === false)
2745
+ this._drag = null;
2746
+ if (this._drag)
2747
+ return;
2748
+ this._clearPreview();
2749
+ }
2750
+ _cellDate(target) {
2751
+ const cell = target instanceof Element ? target.closest(".dp-day[data-date]") : null;
2752
+ const date = cell?.getAttribute("data-date") || "";
2753
+ return isDate(date) ? date : "";
2754
+ }
2755
+ _dateAtPoint(x, y) {
2756
+ const element = this.ownerDocument.elementFromPoint(x, y);
2757
+ if (!(element instanceof Element) || !this._calendar?.contains(element))
2758
+ return "";
2759
+ return this._cellDate(element);
2760
+ }
2761
+ _onGridPointerDown(event) {
2762
+ const pointer = event;
2763
+ this._suppressGridClick = false;
2764
+ if (this._drag?.active === false)
2765
+ this._drag = null;
2766
+ if (!this._open || !this._range || this._drag)
2767
+ return;
2768
+ if (!pointer.isPrimary || pointer.button !== 0)
2769
+ return;
2770
+ if (pointer.pointerType !== "mouse" && pointer.pointerType !== "pen")
2771
+ return;
2772
+ if (!this._range.complete)
2773
+ return;
2774
+ const cell = pointer.target instanceof Element ? pointer.target.closest(".dp-day[data-date]") : null;
2775
+ if (!(cell instanceof HTMLElement))
2776
+ return;
2777
+ const isStart = cell.hasAttribute("data-range-start");
2778
+ const isEnd = cell.hasAttribute("data-range-end");
2779
+ if (isStart === isEnd)
2780
+ return;
2781
+ const endpoint = isStart ? "start" : "end";
2782
+ const date = this._cellDate(cell);
2783
+ if (!date || !this._boundEditable(endpoint))
2784
+ return;
2785
+ this._drag = {
2786
+ pointerId: pointer.pointerId,
2787
+ endpoint,
2788
+ x: pointer.clientX,
2789
+ y: pointer.clientY,
2790
+ date,
2791
+ active: false
2792
+ };
2793
+ }
2794
+ _onGridPointerMove(event) {
2795
+ const pointer = event;
2796
+ const drag = this._drag;
2797
+ if (!drag || pointer.pointerId !== drag.pointerId)
2798
+ return;
2799
+ if (!drag.active) {
2800
+ const travelled = Math.abs(pointer.clientX - drag.x) > DRAG_THRESHOLD || Math.abs(pointer.clientY - drag.y) > DRAG_THRESHOLD;
2801
+ if (!travelled)
2802
+ return;
2803
+ drag.active = true;
2804
+ try {
2805
+ this._calendar?.setPointerCapture(drag.pointerId);
2806
+ } catch {}
2807
+ this._syncDragAffordance();
2808
+ }
2809
+ const date = this._dateAtPoint(pointer.clientX, pointer.clientY);
2810
+ if (!date || this._calendar?.getDateState(date).disabled)
2811
+ return;
2812
+ drag.date = date;
2813
+ this._setPreview(date, drag.endpoint);
2814
+ }
2815
+ _onGridPointerUp(event) {
2816
+ const pointer = event;
2817
+ const drag = this._drag;
2818
+ if (!drag || pointer.pointerId !== drag.pointerId)
2819
+ return;
2820
+ this._drag = null;
2821
+ this._releaseDragCapture(drag.pointerId);
2822
+ if (!drag.active) {
2823
+ this._syncDragAffordance();
2824
+ return;
2825
+ }
2826
+ this._suppressGridClick = true;
2827
+ const transition = this._range?.projectEndpoint(drag.date, drag.endpoint, (bound) => this._boundEditable(bound));
2828
+ if (!transition || transition.status === "refused" || !transition.changedEndpoints.length) {
2829
+ this._syncHighlight();
2830
+ return;
2831
+ }
2832
+ const target = drag.endpoint === "start" ? transition.range.start : transition.range.end;
2833
+ this._pendingIntents.clear();
2834
+ this._pendingIntents.set(target, { id: ++this._rangeCommitId, endpoint: drag.endpoint });
2835
+ this._calendar?.activateDate(target);
2836
+ }
2837
+ _onGridPointerCancel(event) {
2838
+ const pointer = event;
2839
+ const drag = this._drag;
2840
+ if (!drag || pointer.pointerId !== drag.pointerId)
2841
+ return;
2842
+ this._drag = null;
2843
+ this._releaseDragCapture(drag.pointerId);
2844
+ this._syncHighlight();
2845
+ }
2846
+ _releaseDragCapture(pointerId) {
2847
+ const calendar = this._calendar;
2848
+ if (calendar?.hasPointerCapture(pointerId))
2849
+ calendar.releasePointerCapture(pointerId);
2850
+ }
2851
+ _syncDragAffordance() {
2852
+ const calendar = this._calendar;
2853
+ if (!calendar || !this._range)
2854
+ return;
2855
+ if (this._drag?.active) {
2856
+ calendar.setAttribute("data-range-drag", "active");
2857
+ return;
2858
+ }
2859
+ const { start, end } = this._range;
2860
+ const draggable = this._range.complete && start !== end && (this._boundEditable("start") || this._boundEditable("end"));
2861
+ if (draggable)
2862
+ calendar.setAttribute("data-range-drag", "ready");
2863
+ else
2864
+ calendar.removeAttribute("data-range-drag");
2404
2865
  }
2405
2866
  _onEscape(event) {
2406
2867
  if (event.key === "Escape" && this._open) {
@@ -2462,7 +2923,8 @@
2462
2923
  this._refreshButtonLabel();
2463
2924
  }
2464
2925
  _refreshButtonLabel() {
2465
- if (!this._button)
2926
+ const single = this._buttons[0]?.button;
2927
+ if (!single)
2466
2928
  return;
2467
2929
  if (this._rangeMode()) {
2468
2930
  this._refreshRangeButtonLabel();
@@ -2470,16 +2932,19 @@
2470
2932
  }
2471
2933
  const prefix = this._value ? this._messages.changeDate : this._messages.chooseDate;
2472
2934
  const suffix = this._value ? `, ${formatLongDate(this._value, this.locale)}` : "";
2473
- this._button.setAttribute("aria-label", `${prefix}${suffix}`);
2935
+ single.setAttribute("aria-label", `${prefix}${suffix}`);
2474
2936
  }
2475
2937
  _refreshRangeButtonLabel() {
2476
- if (!this._button || !this._range)
2938
+ if (!this._range)
2477
2939
  return;
2478
2940
  const { start, end } = this._range;
2479
- const hasRange = Boolean(start && end);
2480
- const prefix = hasRange ? this._messages.changeDate : this._messages.chooseDate;
2481
- const suffix = hasRange ? `, ${formatLongDate(start, this.locale)} – ${formatLongDate(end, this.locale)}` : "";
2482
- this._button.setAttribute("aria-label", `${prefix}${suffix}`);
2941
+ for (const { button, endpoint } of this._buttons) {
2942
+ const bound = endpoint === "end" ? end : start;
2943
+ const prefix = bound ? this._messages.changeDate : this._messages.chooseDate;
2944
+ const suffix = bound ? `, ${formatLongDate(bound, this.locale)}` : "";
2945
+ const boundLabel = endpoint === "end" ? this._messages.rangeEnd : this._messages.rangeStart;
2946
+ button.setAttribute("aria-label", `${prefix}${suffix}, ${boundLabel}`);
2947
+ }
2483
2948
  }
2484
2949
  _reflectValue(value) {
2485
2950
  this._reflecting = true;
@@ -2499,7 +2964,7 @@
2499
2964
  this._field?.setCanonical(this._value, { format: options.format !== false });
2500
2965
  if (this._calendar) {
2501
2966
  this._calendar.value = this._value;
2502
- if (this._value)
2967
+ if (this._value && options.moveDisplay !== false)
2503
2968
  this._calendar.focusedDate = this._value;
2504
2969
  }
2505
2970
  this._refreshButtonLabel();
@@ -2512,13 +2977,15 @@
2512
2977
  }
2513
2978
  }
2514
2979
  }
2515
- async _confirmDate(date) {
2980
+ async _confirmDate(date, options = {}) {
2516
2981
  const calendar = this._calendar;
2517
2982
  if (!calendar)
2518
2983
  return { ok: true };
2519
- const display = monthKey(date);
2520
- if (calendar.display !== display)
2521
- calendar.display = display;
2984
+ if (options.moveDisplay !== false) {
2985
+ const display = monthKey(date);
2986
+ if (calendar.display !== display)
2987
+ calendar.display = display;
2988
+ }
2522
2989
  const confirmed = await calendar.ensureDate(date);
2523
2990
  const state = calendar.getDateState(date);
2524
2991
  if (!confirmed || state.disabled)
@@ -2531,10 +2998,14 @@
2531
2998
  return false;
2532
2999
  if (!options.force && !field.isDirty)
2533
3000
  return false;
2534
- const result = await field.commit();
3001
+ const result = await field.commit(options);
2535
3002
  if (result.status === "stale" || result.status === "invalid")
2536
3003
  return false;
2537
- this._setValue(result.value || "", { emit, format: result.status === "ok" });
3004
+ this._setValue(result.value || "", {
3005
+ emit,
3006
+ format: result.status === "ok",
3007
+ moveDisplay: options.moveDisplay
3008
+ });
2538
3009
  return true;
2539
3010
  }
2540
3011
  async _commitFieldText(which) {
@@ -2552,7 +3023,7 @@
2552
3023
  this._orderTags.delete(index);
2553
3024
  return false;
2554
3025
  }
2555
- this._setBound(which, result.value || "", { emit: true, user: true, format: result.status === "ok" });
3026
+ this._setBound(which, result.value || "", { emit: false, user: true, format: result.status === "ok" });
2556
3027
  return true;
2557
3028
  }
2558
3029
  _setBound(which, value, options = {}) {
@@ -2609,19 +3080,21 @@
2609
3080
  return;
2610
3081
  this.dispatchEvent(new CustomEvent("rangechange", { detail: { ...this._range.range }, bubbles: true }));
2611
3082
  }
3083
+ _displayRange() {
3084
+ const { start, end } = this._range?.range ?? { start: "", end: "" };
3085
+ if (!start)
3086
+ return { start: "", end: "" };
3087
+ if (end && compareDates(end, start) < 0)
3088
+ return { start, end: "" };
3089
+ return { start, end };
3090
+ }
2612
3091
  _syncHighlight() {
2613
3092
  if (!this._calendar || !this._range)
2614
3093
  return;
2615
- const { start, end } = this._range;
2616
- if (!start) {
2617
- this._calendar.highlightedRange = { start: "", end: "" };
2618
- return;
2619
- }
2620
- if (end && compareDates(end, start) < 0) {
2621
- this._calendar.highlightedRange = { start, end: "" };
2622
- } else {
2623
- this._calendar.highlightedRange = { start, end };
2624
- }
3094
+ this._previewDate = "";
3095
+ this._calendar.removeAttribute("data-range-preview");
3096
+ this._calendar.highlightedRange = this._displayRange();
3097
+ this._syncDragAffordance();
2625
3098
  }
2626
3099
  _revalidateRange(which) {
2627
3100
  if (!this._fields || !this._range)
@@ -2711,10 +3184,14 @@
2711
3184
  this._suppressFocusOpen = false;
2712
3185
  }, 0);
2713
3186
  }
2714
- _focusInput() {
3187
+ _restoreFocus() {
3188
+ const target = this._returnFocus;
3189
+ this._returnFocus = null;
3190
+ if (!(target instanceof HTMLElement) || !target.isConnected || target.matches(":disabled"))
3191
+ return;
2715
3192
  this._suppressFocusOpen = true;
2716
3193
  setTimeout(() => {
2717
- this._input?.focus();
3194
+ target.focus();
2718
3195
  this._suppressFocusOpen = false;
2719
3196
  }, 0);
2720
3197
  }
@@ -2739,7 +3216,7 @@
2739
3216
  if (!fields)
2740
3217
  return true;
2741
3218
  for (const [index, field] of fields.entries()) {
2742
- const result = await field.commit();
3219
+ const result = await field.commit({ moveDisplay: false });
2743
3220
  if (result.status === "ok" || result.status === "clear") {
2744
3221
  const which = index === 1 ? "end" : "start";
2745
3222
  this._setBound(which, result.value || "", { format: result.status === "ok" });
@@ -2755,7 +3232,7 @@
2755
3232
  if (!input.value.trim()) {
2756
3233
  input.setCustomValidity("");
2757
3234
  } else {
2758
- await this._commitText(false, { force: true });
3235
+ await this._commitText(false, { force: true, moveDisplay: false });
2759
3236
  }
2760
3237
  this._revalidateTimeOrder("start");
2761
3238
  this._revalidateTimeOrder("end");
@@ -2765,20 +3242,13 @@
2765
3242
  show(options = {}) {
2766
3243
  const panel = this._panel;
2767
3244
  const calendar = this._calendar;
2768
- const button = this._button;
2769
- if (!panel || !calendar || !button || this._open)
3245
+ if (!panel || !calendar || !this._buttons.length || this._open)
2770
3246
  return;
2771
3247
  if (this._rangeMode()) {
2772
- const endpoint = this._resolveRangeEndpoint();
3248
+ const endpoint = options.endpoint || this._resolveRangeEndpoint();
2773
3249
  if (!endpoint)
2774
3250
  return;
2775
- this._range?.focus(endpoint);
2776
- this._rangeCommitId++;
2777
- this._pendingIntents.clear();
2778
- const target = this._boundCanonical(endpoint) || todayISO();
2779
- calendar.display = monthKey(target);
2780
- calendar.focusedDate = target;
2781
- this._lastFocusEndpoint = endpoint;
3251
+ this._activateRangeEndpoint(endpoint);
2782
3252
  } else {
2783
3253
  const input = this._input;
2784
3254
  if (!input || input.disabled || input.readOnly)
@@ -2786,11 +3256,19 @@
2786
3256
  const target = this._value || this._adapter().parse(input.value) || todayISO();
2787
3257
  calendar.display = monthKey(target);
2788
3258
  calendar.focusedDate = target;
3259
+ this._returnFocus = options.returnFocus instanceof HTMLElement ? options.returnFocus : input;
2789
3260
  }
2790
3261
  panel.showPopover();
2791
3262
  this._open = true;
2792
3263
  this._setExpanded(true);
2793
- const position = () => reposition(this, panel, { placement: "bottom-start", distance: 4, shiftPadding: 8 });
3264
+ const coordinateSpace = this.coordinateSpace;
3265
+ panel.style.position = coordinateSpace === "document" ? "absolute" : "fixed";
3266
+ const position = () => reposition(this, panel, {
3267
+ placement: "bottom-start",
3268
+ distance: 4,
3269
+ shiftPadding: 8,
3270
+ coordinateSpace
3271
+ });
2794
3272
  position();
2795
3273
  this._stopTracking = autoUpdate(this, panel, position);
2796
3274
  if (options.moveFocus !== false)
@@ -2800,7 +3278,8 @@
2800
3278
  _setExpanded(expanded) {
2801
3279
  const value = expanded ? "true" : "false";
2802
3280
  this._input?.setAttribute("aria-expanded", value);
2803
- this._button?.setAttribute("aria-expanded", value);
3281
+ for (const { button } of this._buttons)
3282
+ button.setAttribute("aria-expanded", value);
2804
3283
  for (const field of this._fields || [])
2805
3284
  field.input.setAttribute("aria-expanded", value);
2806
3285
  }
@@ -2816,13 +3295,19 @@
2816
3295
  this._setExpanded(false);
2817
3296
  this._rangeCommitId++;
2818
3297
  this._pendingIntents.clear();
3298
+ this._suppressGridClick = false;
3299
+ if (this._drag) {
3300
+ this._releaseDragCapture(this._drag.pointerId);
3301
+ this._drag = null;
3302
+ }
3303
+ this._syncHighlight();
2819
3304
  if (restoreFocus) {
2820
3305
  if (this._rangeMode()) {
2821
3306
  const endpoint = this._restoreFocusEndpoint();
2822
3307
  if (endpoint)
2823
3308
  this._focusField(endpoint);
2824
3309
  } else {
2825
- this._focusInput();
3310
+ this._restoreFocus();
2826
3311
  }
2827
3312
  }
2828
3313
  this.dispatchEvent(new Event("close", { bubbles: true }));