@lekoala/date-picker 0.1.1 → 0.3.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.1.1 - https://github.com/lekoala/date-picker ***/
1
+ /*** @lekoala/date-picker v0.3.0 - https://github.com/lekoala/date-picker ***/
2
2
  (() => {
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -387,6 +387,32 @@
387
387
  timeZone: "UTC"
388
388
  }).format(toIntlDate(`${value.slice(0, 7)}-15`));
389
389
  }
390
+ function monthYearOrder(locale = "") {
391
+ const resolved = resolveLocale(locale);
392
+ try {
393
+ const parts = new Intl.DateTimeFormat(resolved, {
394
+ calendar: "gregory",
395
+ year: "numeric",
396
+ month: "long",
397
+ timeZone: "UTC"
398
+ }).formatToParts(toIntlDate("2026-09-15"));
399
+ let monthIndex = -1;
400
+ let yearIndex = -1;
401
+ parts.forEach((part, index) => {
402
+ const type = part.type;
403
+ if (type === "month") {
404
+ if (monthIndex < 0)
405
+ monthIndex = index;
406
+ } else if (type === "year" || type === "relatedYear" || type === "yearName") {
407
+ if (yearIndex < 0)
408
+ yearIndex = index;
409
+ }
410
+ });
411
+ if (monthIndex >= 0 && yearIndex >= 0)
412
+ return yearIndex < monthIndex ? ["year", "month"] : ["month", "year"];
413
+ } catch {}
414
+ return ["month", "year"];
415
+ }
390
416
  function monthNames(locale = "", style = "long") {
391
417
  const resolved = resolveLocale(locale);
392
418
  const formatter = new Intl.DateTimeFormat(resolved, { calendar: "gregory", month: style, timeZone: "UTC" });
@@ -567,7 +593,8 @@
567
593
  "locale",
568
594
  "fixed-weeks",
569
595
  "show-week-numbers",
570
- "selection"
596
+ "selection",
597
+ "month-format"
571
598
  ];
572
599
  constructor() {
573
600
  super();
@@ -689,6 +716,15 @@
689
716
  set selection(value) {
690
717
  this.setAttribute("selection", value === "none" ? "none" : "single");
691
718
  }
719
+ get monthFormat() {
720
+ return this.getAttribute("month-format") === "short" ? "short" : "long";
721
+ }
722
+ set monthFormat(value) {
723
+ if (value === "short")
724
+ this.setAttribute("month-format", "short");
725
+ else
726
+ this.removeAttribute("month-format");
727
+ }
692
728
  get highlightedRange() {
693
729
  return { ...this._highlightedRange };
694
730
  }
@@ -798,7 +834,7 @@
798
834
  return weeks;
799
835
  }
800
836
  _focusTargetKey(element) {
801
- if (element.matches(".dp-month-select, .dp-year-select, .dp-year-input") && element.id) {
837
+ if (element.matches(".dp-month-select, .dp-year-input") && element.id) {
802
838
  return `#${CSS.escape(element.id)}`;
803
839
  }
804
840
  if (element.matches(".dp-nav[data-calendar-action]")) {
@@ -1040,10 +1076,6 @@
1040
1076
  this._setDisplay(`${this.display.slice(0, 4)}-${target.value}`);
1041
1077
  return;
1042
1078
  }
1043
- if (target instanceof HTMLSelectElement && target.matches(".dp-year-select")) {
1044
- this._setDisplay(`${target.value}-${this.display.slice(5, 7)}`);
1045
- return;
1046
- }
1047
1079
  if (target instanceof HTMLInputElement && target.matches(".dp-year-input")) {
1048
1080
  const year = Number(target.value);
1049
1081
  if (!Number.isInteger(year) || year < 1 || year > 9999) {
@@ -1109,7 +1141,7 @@
1109
1141
  const display = this.display;
1110
1142
  const displayYear = yearOf(display);
1111
1143
  const displayMonth = Number(display.slice(5, 7));
1112
- const names = monthNames(locale, "long");
1144
+ const names = monthNames(locale, this.monthFormat);
1113
1145
  const weekdayShort = weekdayNames(locale, this.firstDay, "short");
1114
1146
  const weekdayLong = weekdayNames(locale, this.firstDay, "long");
1115
1147
  const weeks = this._weeks();
@@ -1154,14 +1186,18 @@
1154
1186
  }).join("");
1155
1187
  const focusedElement = document.activeElement;
1156
1188
  const focusKey = focusedElement instanceof HTMLElement && this.contains(focusedElement) ? this._focusTargetKey(focusedElement) : "";
1189
+ const yearFirst = monthYearOrder(locale)[0] === "year";
1190
+ this.toggleAttribute("data-year-first", yearFirst);
1191
+ const monthControl = `<label class="dp-visually-hidden" for="${this._id}-month">${escapeHtml(this._messages.month)}</label>
1192
+ <select id="${this._id}-month" class="dp-month-select" aria-label="${escapeHtml(this._messages.month)}">${monthOptions}</select>`;
1193
+ const yearControl = `<label class="dp-visually-hidden" for="${this._id}-year">${escapeHtml(this._messages.year)}</label>
1194
+ <input id="${this._id}-year" class="dp-year-input" type="number" inputmode="numeric" value="${displayYear}"${minYearAttr}${maxYearAttr} aria-label="${escapeHtml(this._messages.year)}">`;
1157
1195
  this._rendering = true;
1158
1196
  this.innerHTML = `
1159
1197
  <div class="dp-calendar-shell">
1160
1198
  <div class="dp-calendar-header">
1161
- <label class="dp-visually-hidden" for="${this._id}-month">${escapeHtml(this._messages.month)}</label>
1162
- <select id="${this._id}-month" class="dp-month-select" aria-label="${escapeHtml(this._messages.month)}">${monthOptions}</select>
1163
- <label class="dp-visually-hidden" for="${this._id}-year">${escapeHtml(this._messages.year)}</label>
1164
- <input id="${this._id}-year" class="dp-year-input" type="number" inputmode="numeric" value="${displayYear}"${minYearAttr}${maxYearAttr} aria-label="${escapeHtml(this._messages.year)}">
1199
+ ${yearFirst ? yearControl : monthControl}
1200
+ ${yearFirst ? monthControl : yearControl}
1165
1201
  <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>
1166
1202
  <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>
1167
1203
  </div>
@@ -1174,7 +1210,11 @@
1174
1210
  this._rendering = false;
1175
1211
  if (focusKey) {
1176
1212
  queueMicrotask(() => {
1177
- const target = this.querySelector(focusKey);
1213
+ let selector = focusKey;
1214
+ if (selector.startsWith(".dp-day[data-date=")) {
1215
+ selector = `.dp-day[data-date="${CSS.escape(this._model.focused)}"]`;
1216
+ }
1217
+ const target = this.querySelector(selector);
1178
1218
  if (target instanceof HTMLElement)
1179
1219
  target.focus();
1180
1220
  });
@@ -1516,8 +1556,11 @@
1516
1556
  const availableHeight = `${getAvailableHeight(referenceRect, side, boundary, distance, shiftPadding)}px`;
1517
1557
  const { style } = floating;
1518
1558
  const roomChanged = style.getPropertyValue("--available-height") !== availableHeight;
1519
- style.left = `${coords.x}px`;
1520
- style.top = `${coords.y}px`;
1559
+ const win = options.coordinateSpace === "document" ? reference.ownerDocument.defaultView : null;
1560
+ const originX = win ? win.scrollX : 0;
1561
+ const originY = win ? win.scrollY : 0;
1562
+ style.left = `${coords.x + originX}px`;
1563
+ style.top = `${coords.y + originY}px`;
1521
1564
  style.setProperty("--arrow-x", `${arrowX}%`);
1522
1565
  style.setProperty("--arrow-y", `${arrowY}%`);
1523
1566
  style.setProperty("--available-height", availableHeight);
@@ -1624,16 +1667,23 @@
1624
1667
  }
1625
1668
  handleInput() {
1626
1669
  const input = this.input;
1627
- this._dirty = true;
1670
+ const text = input.value.trim();
1671
+ const canonical = this.canonical ? this.adapter.format(this.canonical) : "";
1628
1672
  this.dirty();
1673
+ if (text === canonical) {
1674
+ this._dirty = false;
1675
+ if (this.hidden)
1676
+ this.hidden.value = this.canonical;
1677
+ input.setCustomValidity("");
1678
+ return;
1679
+ }
1680
+ this._dirty = true;
1629
1681
  if (this.hidden)
1630
1682
  this.hidden.value = "";
1631
- const text = input.value.trim();
1632
1683
  if (!text) {
1633
1684
  input.setCustomValidity("");
1634
1685
  return;
1635
1686
  }
1636
- const canonical = this.canonical ? this.adapter.format(this.canonical) : "";
1637
1687
  input.setCustomValidity(text === canonical ? "" : this.messages.invalidDate);
1638
1688
  }
1639
1689
  async commit() {
@@ -1697,11 +1747,40 @@
1697
1747
  }
1698
1748
  }
1699
1749
 
1750
+ // src/time.js
1751
+ var TIME_RE = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?$/;
1752
+ function parseTimeParts(value) {
1753
+ const match = TIME_RE.exec(String(value || ""));
1754
+ if (!match)
1755
+ return null;
1756
+ const hour = Number(match[1]);
1757
+ const minute = Number(match[2]);
1758
+ const second = match[3] === undefined ? 0 : Number(match[3]);
1759
+ const millisecond = match[4] === undefined ? 0 : Number(match[4].padEnd(3, "0"));
1760
+ if (hour > 23 || minute > 59 || second > 59 || millisecond > 999)
1761
+ return null;
1762
+ return { hour, minute, second, millisecond };
1763
+ }
1764
+ function isTime(value) {
1765
+ return parseTimeParts(value) !== null;
1766
+ }
1767
+ function toMillis(parts) {
1768
+ return ((parts.hour * 60 + parts.minute) * 60 + parts.second) * 1000 + parts.millisecond;
1769
+ }
1770
+ function compareTimes(a, b) {
1771
+ const left = parseTimeParts(a);
1772
+ const right = parseTimeParts(b);
1773
+ if (!left || !right)
1774
+ throw new TypeError("compareTimes() expects HH:MM values");
1775
+ const diff = toMillis(left) - toMillis(right);
1776
+ return diff === 0 ? 0 : diff < 0 ? -1 : 1;
1777
+ }
1778
+
1700
1779
  // src/date-picker.js
1701
1780
  var uid2 = 0;
1702
1781
 
1703
1782
  class DatePickerElement extends HTMLElement {
1704
- static observedAttributes = ["value", "locale", "min", "max", "open-on-focus"];
1783
+ static observedAttributes = ["value", "locale", "min", "max", "open-on-focus", "month-format"];
1705
1784
  constructor() {
1706
1785
  super();
1707
1786
  this._id = `date-picker-${++uid2}`;
@@ -1713,10 +1792,13 @@
1713
1792
  this._fields = null;
1714
1793
  this._range = null;
1715
1794
  this._orderTags = new Set;
1795
+ this._timeFields = [null, null];
1796
+ this._timeOrderOwned = new Set;
1716
1797
  this._lastFocusEndpoint = "";
1717
1798
  this._rangeCommitId = 0;
1718
1799
  this._pendingIntents = new Map;
1719
- this._button = null;
1800
+ this._buttons = [];
1801
+ this._returnFocus = null;
1720
1802
  this._panel = null;
1721
1803
  this._calendar = null;
1722
1804
  this._originalDescribedBy = "";
@@ -1732,6 +1814,7 @@
1732
1814
  this._dateState = null;
1733
1815
  this._renderDay = null;
1734
1816
  this._isDateDisabled = null;
1817
+ this._coordinateSpace = "viewport";
1735
1818
  }
1736
1819
  _rangeMode() {
1737
1820
  return this.hasAttribute("range") && Boolean(this._range);
@@ -1746,13 +1829,14 @@
1746
1829
  this._connectSingle();
1747
1830
  }
1748
1831
  _connectSingle() {
1749
- const input = this.querySelector(":scope > input:not([type=hidden])");
1832
+ const input = this.querySelector(":scope > input:not([type=hidden]):not([type=time]):not([data-time-start]):not([data-time-end])");
1750
1833
  if (!(input instanceof HTMLInputElement)) {
1751
1834
  console.warn("<date-picker> expects a direct child text input");
1752
1835
  return;
1753
1836
  }
1754
1837
  this._connected = true;
1755
1838
  this._input = input;
1839
+ this._discoverTimeFields();
1756
1840
  this._field = new DateFieldController(input, { messages: this._messages, locale: this.locale });
1757
1841
  this._field.onAttributesChanged = () => this._syncInputState();
1758
1842
  this._field.confirm = (date) => this._confirmDate(date);
@@ -1775,7 +1859,31 @@
1775
1859
  input.defaultValue = input.value;
1776
1860
  }
1777
1861
  }
1862
+ _discoverTimeFields() {
1863
+ this._timeFields = [null, null];
1864
+ const markers = ["data-time-start", "data-time-end"];
1865
+ for (const [index, marker] of markers.entries()) {
1866
+ const matches = this.querySelectorAll(`:scope > input[${marker}]`);
1867
+ if (matches.length > 1) {
1868
+ console.warn(`<date-picker> expects at most one direct child input[${marker}]; ignoring extras`);
1869
+ }
1870
+ const candidate = matches[0];
1871
+ if (candidate == null)
1872
+ continue;
1873
+ if (!(candidate instanceof HTMLInputElement) || candidate.type !== "time") {
1874
+ console.warn(`<date-picker> input[${marker}] must be an <input type="time">; ignoring it`);
1875
+ continue;
1876
+ }
1877
+ this._timeFields[index] = candidate;
1878
+ }
1879
+ if (this.querySelector(":scope > input[type=time]:not([data-time-start]):not([data-time-end])")) {
1880
+ console.warn("<date-picker> time inputs need [data-time-start] or [data-time-end]; ignoring it");
1881
+ }
1882
+ }
1778
1883
  _connectRange() {
1884
+ if (this.querySelector(":scope > input[data-time-start], :scope > input[data-time-end]")) {
1885
+ console.warn("<date-picker range> time companions are not supported yet; leaving them native");
1886
+ }
1779
1887
  const startInput = this.querySelector(":scope > input[data-range-start]");
1780
1888
  const endInput = this.querySelector(":scope > input[data-range-end]");
1781
1889
  if (!(startInput instanceof HTMLInputElement) || !(endInput instanceof HTMLInputElement)) {
@@ -1798,7 +1906,7 @@
1798
1906
  field.confirm = (date) => this._confirmDate(date);
1799
1907
  field.setupFormValue();
1800
1908
  }
1801
- this._build(endInput);
1909
+ this._build();
1802
1910
  this._setRangeAria();
1803
1911
  this._syncRangeFields();
1804
1912
  this._bindRange();
@@ -1827,6 +1935,8 @@
1827
1935
  this.hide(false);
1828
1936
  this._controller?.abort();
1829
1937
  this._controller = null;
1938
+ this._clearTimeOrderValidity();
1939
+ this._timeFields = [null, null];
1830
1940
  if (this._rangeMode()) {
1831
1941
  for (const field of this._fields || [])
1832
1942
  this._teardownField(field);
@@ -1836,10 +1946,12 @@
1836
1946
  this._teardownField(this._field);
1837
1947
  }
1838
1948
  this._field = null;
1839
- this._button?.remove();
1949
+ for (const { button } of this._buttons)
1950
+ button.remove();
1951
+ this._buttons = [];
1952
+ this._returnFocus = null;
1840
1953
  this._panel?.remove();
1841
1954
  this._formatHint?.remove();
1842
- this._button = null;
1843
1955
  this._panel = null;
1844
1956
  this._calendar = null;
1845
1957
  this._formatHint = null;
@@ -1911,6 +2023,15 @@
1911
2023
  else
1912
2024
  this.removeAttribute("locale");
1913
2025
  }
2026
+ get monthFormat() {
2027
+ return this.getAttribute("month-format") === "short" ? "short" : "long";
2028
+ }
2029
+ set monthFormat(value) {
2030
+ if (value === "short")
2031
+ this.setAttribute("month-format", "short");
2032
+ else
2033
+ this.removeAttribute("month-format");
2034
+ }
1914
2035
  get min() {
1915
2036
  const value = this.getAttribute("min") || "";
1916
2037
  return isDate(value) ? value : "";
@@ -1994,6 +2115,12 @@
1994
2115
  if (this._connected)
1995
2116
  this.validate();
1996
2117
  }
2118
+ get coordinateSpace() {
2119
+ return this._coordinateSpace;
2120
+ }
2121
+ set coordinateSpace(value) {
2122
+ this._coordinateSpace = value === "document" ? "document" : "viewport";
2123
+ }
1997
2124
  _adapter() {
1998
2125
  return this._field?.adapter ?? this._fields?.[0]?.adapter ?? createDateAdapter(this.locale);
1999
2126
  }
@@ -2005,8 +2132,9 @@
2005
2132
  if (!input)
2006
2133
  return;
2007
2134
  this._field?.syncInputState();
2008
- if (this._button)
2009
- this._button.disabled = input.disabled || input.readOnly;
2135
+ const button = this._buttons[0]?.button;
2136
+ if (button)
2137
+ button.disabled = input.disabled || input.readOnly;
2010
2138
  if (this._open && (input.disabled || input.readOnly))
2011
2139
  this.hide(false);
2012
2140
  }
@@ -2015,10 +2143,12 @@
2015
2143
  return;
2016
2144
  for (const field of this._fields)
2017
2145
  field.syncInputState();
2146
+ for (const { button, endpoint } of this._buttons) {
2147
+ const index = endpoint === "end" ? 1 : 0;
2148
+ const field = this._fields[index];
2149
+ button.disabled = Boolean(field?.input.disabled || field?.input.readOnly);
2150
+ }
2018
2151
  const active = this._range?.activeEndpoint === "end" ? 1 : 0;
2019
- const disabled = this._fields.every((field) => field.input.disabled || field.input.readOnly);
2020
- if (this._button)
2021
- this._button.disabled = disabled;
2022
2152
  if (this._open && (this._fields[active]?.input.disabled || this._fields[active]?.input.readOnly)) {
2023
2153
  this.hide(false);
2024
2154
  }
@@ -2036,19 +2166,37 @@
2036
2166
  input.setAttribute("aria-controls", this._panel.id);
2037
2167
  }
2038
2168
  }
2039
- _build(anchorInput) {
2040
- const input = anchorInput || this._input;
2041
- if (!input)
2042
- return;
2043
- const panelId = `${this._id}-panel`;
2044
- const hintId = `${this._id}-format`;
2169
+ _createTrigger(endpoint) {
2045
2170
  const button = document.createElement("button");
2046
2171
  button.type = "button";
2047
2172
  button.className = "dp-picker-button";
2048
2173
  button.setAttribute("aria-haspopup", "dialog");
2049
2174
  button.setAttribute("aria-expanded", "false");
2050
- button.setAttribute("aria-controls", panelId);
2175
+ button.setAttribute("aria-controls", `${this._id}-panel`);
2176
+ if (endpoint)
2177
+ button.dataset.endpoint = endpoint;
2051
2178
  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>';
2179
+ return button;
2180
+ }
2181
+ _build() {
2182
+ const ranges = this._rangeMode();
2183
+ const targets = [];
2184
+ if (ranges) {
2185
+ for (const [index, field] of (this._fields || []).entries()) {
2186
+ targets.push([field.input, index === 1 ? "end" : "start"]);
2187
+ }
2188
+ } else if (this._input) {
2189
+ targets.push([this._input, ""]);
2190
+ }
2191
+ if (!targets.length)
2192
+ return;
2193
+ const panelId = `${this._id}-panel`;
2194
+ const hintId = `${this._id}-format`;
2195
+ this._buttons = targets.map(([input, endpoint]) => {
2196
+ const button = this._createTrigger(endpoint);
2197
+ input.insertAdjacentElement("afterend", button);
2198
+ return { button, endpoint };
2199
+ });
2052
2200
  const panel = document.createElement("div");
2053
2201
  panel.id = panelId;
2054
2202
  panel.className = "dp-picker-panel";
@@ -2062,28 +2210,25 @@
2062
2210
  const hint = document.createElement("span");
2063
2211
  hint.id = hintId;
2064
2212
  hint.className = "dp-visually-hidden";
2065
- input.insertAdjacentElement("afterend", button);
2066
- button.insertAdjacentElement("afterend", panel);
2067
- panel.insertAdjacentElement("afterend", hint);
2068
- this._button = button;
2213
+ this.append(panel, hint);
2069
2214
  this._panel = panel;
2070
2215
  this._calendar = calendar;
2071
2216
  this._formatHint = hint;
2072
- if (!this._rangeMode()) {
2073
- this._originalDescribedBy = input.getAttribute("aria-describedby") || "";
2217
+ if (!ranges && this._input) {
2218
+ this._originalDescribedBy = this._input.getAttribute("aria-describedby") || "";
2074
2219
  const describedBy = [this._originalDescribedBy, hintId].filter(Boolean).join(" ");
2075
- input.setAttribute("aria-describedby", describedBy);
2076
- input.setAttribute("role", "combobox");
2077
- input.setAttribute("aria-haspopup", "dialog");
2078
- input.setAttribute("aria-expanded", "false");
2079
- input.setAttribute("aria-controls", panelId);
2220
+ this._input.setAttribute("aria-describedby", describedBy);
2221
+ this._input.setAttribute("role", "combobox");
2222
+ this._input.setAttribute("aria-haspopup", "dialog");
2223
+ this._input.setAttribute("aria-expanded", "false");
2224
+ this._input.setAttribute("aria-controls", panelId);
2080
2225
  }
2081
2226
  this._refreshLocale();
2082
2227
  this._refreshButtonLabel();
2083
2228
  }
2084
2229
  _bind() {
2085
2230
  const input = this._input;
2086
- const button = this._button;
2231
+ const button = this._buttons[0]?.button;
2087
2232
  const calendar = this._calendar;
2088
2233
  if (!input || !button || !calendar)
2089
2234
  return;
@@ -2107,6 +2252,13 @@
2107
2252
  input.addEventListener("keydown", (event) => {
2108
2253
  this._onFieldKeyDown("", event);
2109
2254
  }, { signal });
2255
+ for (const [index, timeInput] of this._timeFields.entries()) {
2256
+ if (!timeInput)
2257
+ continue;
2258
+ const endpoint = index === 1 ? "end" : "start";
2259
+ timeInput.addEventListener("input", () => this._revalidateTimeOrder(endpoint), { signal });
2260
+ timeInput.addEventListener("change", () => this._revalidateTimeOrder(endpoint), { signal });
2261
+ }
2110
2262
  button.addEventListener("click", (event) => {
2111
2263
  if (this._open) {
2112
2264
  if (event.detail === 0) {
@@ -2116,8 +2268,9 @@
2116
2268
  this.hide(false);
2117
2269
  return;
2118
2270
  }
2119
- this.show();
2271
+ this.show({ returnFocus: button });
2120
2272
  }, { signal });
2273
+ button.addEventListener("keydown", (event) => this._onTriggerKeyDown("", event), { signal });
2121
2274
  calendar.addEventListener("dateactivate", (event) => {
2122
2275
  const custom = event;
2123
2276
  const date = custom.detail?.date;
@@ -2131,7 +2284,7 @@
2131
2284
  this._field?.dirty();
2132
2285
  this._setValue(date, { emit: true, format: true });
2133
2286
  this.hide(false);
2134
- this._focusInput();
2287
+ this._restoreFocus();
2135
2288
  });
2136
2289
  }, { signal });
2137
2290
  calendar.addEventListener("dateloadend", () => void this.validate(), { signal });
@@ -2153,9 +2306,8 @@
2153
2306
  this.addEventListener("keydown", (event) => this._onEscape(event), { signal });
2154
2307
  }
2155
2308
  _bindRange() {
2156
- const button = this._button;
2157
2309
  const calendar = this._calendar;
2158
- if (!this._fields || !button || !calendar)
2310
+ if (!this._fields || !this._buttons.length || !calendar)
2159
2311
  return;
2160
2312
  const controller = new AbortController;
2161
2313
  this._controller = controller;
@@ -2168,17 +2320,17 @@
2168
2320
  field.input.addEventListener("focus", (event) => this._onFieldFocus(endpoint, event), { signal });
2169
2321
  field.input.addEventListener("keydown", (event) => this._onFieldKeyDown(endpoint, event), { signal });
2170
2322
  }
2171
- button.addEventListener("click", (event) => {
2172
- if (this._open) {
2173
- if (event.detail === 0) {
2174
- setTimeout(() => this._calendar?.focusGrid(), 0);
2175
- return;
2176
- }
2177
- this.hide(false);
2178
- return;
2179
- }
2180
- this.show();
2181
- }, { signal });
2323
+ for (const entry of this._buttons) {
2324
+ const endpoint = entry.endpoint;
2325
+ if (endpoint === "")
2326
+ continue;
2327
+ entry.button.addEventListener("click", (event) => this._onRangeTriggerClick(endpoint, event), {
2328
+ signal
2329
+ });
2330
+ entry.button.addEventListener("keydown", (event) => this._onTriggerKeyDown(endpoint, event), {
2331
+ signal
2332
+ });
2333
+ }
2182
2334
  calendar.addEventListener("click", (event) => this._captureGridIntent(event), { capture: true, signal });
2183
2335
  calendar.addEventListener("keydown", (event) => this._captureGridIntent(event), {
2184
2336
  capture: true,
@@ -2233,8 +2385,51 @@
2233
2385
  }, { capture: true, signal });
2234
2386
  this.addEventListener("keydown", (event) => this._onEscape(event), { signal });
2235
2387
  }
2388
+ _activateRangeEndpoint(endpoint) {
2389
+ const calendar = this._calendar;
2390
+ if (!this._range || !calendar)
2391
+ return;
2392
+ this._range.focus(endpoint);
2393
+ this._rangeCommitId++;
2394
+ this._pendingIntents.clear();
2395
+ this._lastFocusEndpoint = endpoint;
2396
+ const target = this._boundCanonical(endpoint) || todayISO();
2397
+ calendar.display = monthKey(target);
2398
+ calendar.focusedDate = target;
2399
+ }
2400
+ _onRangeTriggerClick(endpoint, event) {
2401
+ if (!this._open) {
2402
+ this.show({ endpoint });
2403
+ return;
2404
+ }
2405
+ if (event.detail === 0) {
2406
+ this._activateRangeEndpoint(endpoint);
2407
+ setTimeout(() => this._calendar?.focusGrid(), 0);
2408
+ return;
2409
+ }
2410
+ if (endpoint !== this._range?.activeEndpoint) {
2411
+ this._activateRangeEndpoint(endpoint);
2412
+ return;
2413
+ }
2414
+ this.hide(false);
2415
+ }
2416
+ _onTriggerKeyDown(endpoint, event) {
2417
+ if (event.key !== "ArrowDown")
2418
+ return;
2419
+ event.preventDefault();
2420
+ if (this._open) {
2421
+ if (this._rangeMode() && endpoint)
2422
+ this._activateRangeEndpoint(endpoint);
2423
+ setTimeout(() => this._calendar?.focusGrid(), 0);
2424
+ return;
2425
+ }
2426
+ if (this._rangeMode())
2427
+ this.show({ endpoint: endpoint || undefined });
2428
+ else
2429
+ this.show({ returnFocus: this._buttons[0]?.button });
2430
+ }
2236
2431
  _onFieldKeyDown(endpoint, event) {
2237
- if (event.key === "ArrowDown" || event.altKey && event.key === "ArrowDown") {
2432
+ if (event.key === "ArrowDown") {
2238
2433
  event.preventDefault();
2239
2434
  if (this._rangeMode() && endpoint) {
2240
2435
  this._lastFocusEndpoint = endpoint;
@@ -2282,6 +2477,7 @@
2282
2477
  const date = cell?.getAttribute("data-date") || "";
2283
2478
  if (!isDate(date))
2284
2479
  return;
2480
+ this._pendingIntents.clear();
2285
2481
  this._pendingIntents.set(date, ++this._rangeCommitId);
2286
2482
  }
2287
2483
  _onEscape(event) {
@@ -2299,6 +2495,7 @@
2299
2495
  calendar.locale = this.locale;
2300
2496
  calendar.min = this.min;
2301
2497
  calendar.max = this.max;
2498
+ calendar.monthFormat = this.monthFormat;
2302
2499
  calendar.messages = this._messages;
2303
2500
  calendar.source = this._source;
2304
2501
  calendar.dateState = this._dateState;
@@ -2343,7 +2540,8 @@
2343
2540
  this._refreshButtonLabel();
2344
2541
  }
2345
2542
  _refreshButtonLabel() {
2346
- if (!this._button)
2543
+ const single = this._buttons[0]?.button;
2544
+ if (!single)
2347
2545
  return;
2348
2546
  if (this._rangeMode()) {
2349
2547
  this._refreshRangeButtonLabel();
@@ -2351,16 +2549,19 @@
2351
2549
  }
2352
2550
  const prefix = this._value ? this._messages.changeDate : this._messages.chooseDate;
2353
2551
  const suffix = this._value ? `, ${formatLongDate(this._value, this.locale)}` : "";
2354
- this._button.setAttribute("aria-label", `${prefix}${suffix}`);
2552
+ single.setAttribute("aria-label", `${prefix}${suffix}`);
2355
2553
  }
2356
2554
  _refreshRangeButtonLabel() {
2357
- if (!this._button || !this._range)
2555
+ if (!this._range)
2358
2556
  return;
2359
2557
  const { start, end } = this._range;
2360
- const hasRange = Boolean(start && end);
2361
- const prefix = hasRange ? this._messages.changeDate : this._messages.chooseDate;
2362
- const suffix = hasRange ? `, ${formatLongDate(start, this.locale)} – ${formatLongDate(end, this.locale)}` : "";
2363
- this._button.setAttribute("aria-label", `${prefix}${suffix}`);
2558
+ for (const { button, endpoint } of this._buttons) {
2559
+ const bound = endpoint === "end" ? end : start;
2560
+ const prefix = bound ? this._messages.changeDate : this._messages.chooseDate;
2561
+ const suffix = bound ? `, ${formatLongDate(bound, this.locale)}` : "";
2562
+ const boundLabel = endpoint === "end" ? this._messages.rangeEnd : this._messages.rangeStart;
2563
+ button.setAttribute("aria-label", `${prefix}${suffix}, ${boundLabel}`);
2564
+ }
2364
2565
  }
2365
2566
  _reflectValue(value) {
2366
2567
  this._reflecting = true;
@@ -2433,7 +2634,7 @@
2433
2634
  this._orderTags.delete(index);
2434
2635
  return false;
2435
2636
  }
2436
- this._setBound(which, result.value || "", { emit: true, user: true, format: result.status === "ok" });
2637
+ this._setBound(which, result.value || "", { emit: false, user: true, format: result.status === "ok" });
2437
2638
  return true;
2438
2639
  }
2439
2640
  _setBound(which, value, options = {}) {
@@ -2524,6 +2725,33 @@
2524
2725
  }
2525
2726
  }
2526
2727
  }
2728
+ _revalidateTimeOrder(which) {
2729
+ const [startInput, endInput] = this._timeFields;
2730
+ const bounds = ["start", "end"];
2731
+ const inputs = [startInput, endInput];
2732
+ const startValue = startInput && !startInput.disabled ? startInput.value.trim() : "";
2733
+ const endValue = endInput && !endInput.disabled ? endInput.value.trim() : "";
2734
+ const inverted = Boolean(startInput && endInput && startValue && endValue) && isTime(startValue) && isTime(endValue) && compareTimes(endValue, startValue) < 0;
2735
+ for (const [index, bound] of bounds.entries()) {
2736
+ const input = inputs[index];
2737
+ if (!input)
2738
+ continue;
2739
+ const message = bound === "end" ? this._messages.rangeOrderEnd : this._messages.rangeOrderStart;
2740
+ const shouldHaveOrder = inverted && bound === which;
2741
+ if (shouldHaveOrder) {
2742
+ input.setCustomValidity(message);
2743
+ this._timeOrderOwned.add(input);
2744
+ } else if (this._timeOrderOwned.has(input)) {
2745
+ input.setCustomValidity("");
2746
+ this._timeOrderOwned.delete(input);
2747
+ }
2748
+ }
2749
+ }
2750
+ _clearTimeOrderValidity() {
2751
+ for (const input of this._timeOrderOwned)
2752
+ input.setCustomValidity("");
2753
+ this._timeOrderOwned.clear();
2754
+ }
2527
2755
  _restoreDefault() {
2528
2756
  const field = this._field;
2529
2757
  if (!field)
@@ -2565,10 +2793,14 @@
2565
2793
  this._suppressFocusOpen = false;
2566
2794
  }, 0);
2567
2795
  }
2568
- _focusInput() {
2796
+ _restoreFocus() {
2797
+ const target = this._returnFocus;
2798
+ this._returnFocus = null;
2799
+ if (!(target instanceof HTMLElement) || !target.isConnected || target.matches(":disabled"))
2800
+ return;
2569
2801
  this._suppressFocusOpen = true;
2570
2802
  setTimeout(() => {
2571
- this._input?.focus();
2803
+ target.focus();
2572
2804
  this._suppressFocusOpen = false;
2573
2805
  }, 0);
2574
2806
  }
@@ -2608,28 +2840,24 @@
2608
2840
  return true;
2609
2841
  if (!input.value.trim()) {
2610
2842
  input.setCustomValidity("");
2611
- return input.checkValidity();
2843
+ } else {
2844
+ await this._commitText(false, { force: true });
2612
2845
  }
2613
- await this._commitText(false, { force: true });
2614
- return input.checkValidity();
2846
+ this._revalidateTimeOrder("start");
2847
+ this._revalidateTimeOrder("end");
2848
+ const timesValid = this._timeFields.every((timeInput) => timeInput?.checkValidity() ?? true);
2849
+ return input.checkValidity() && timesValid;
2615
2850
  }
2616
2851
  show(options = {}) {
2617
2852
  const panel = this._panel;
2618
2853
  const calendar = this._calendar;
2619
- const button = this._button;
2620
- if (!panel || !calendar || !button || this._open)
2854
+ if (!panel || !calendar || !this._buttons.length || this._open)
2621
2855
  return;
2622
2856
  if (this._rangeMode()) {
2623
- const endpoint = this._resolveRangeEndpoint();
2857
+ const endpoint = options.endpoint || this._resolveRangeEndpoint();
2624
2858
  if (!endpoint)
2625
2859
  return;
2626
- this._range?.focus(endpoint);
2627
- this._rangeCommitId++;
2628
- this._pendingIntents.clear();
2629
- const target = this._boundCanonical(endpoint) || todayISO();
2630
- calendar.display = monthKey(target);
2631
- calendar.focusedDate = target;
2632
- this._lastFocusEndpoint = endpoint;
2860
+ this._activateRangeEndpoint(endpoint);
2633
2861
  } else {
2634
2862
  const input = this._input;
2635
2863
  if (!input || input.disabled || input.readOnly)
@@ -2637,11 +2865,19 @@
2637
2865
  const target = this._value || this._adapter().parse(input.value) || todayISO();
2638
2866
  calendar.display = monthKey(target);
2639
2867
  calendar.focusedDate = target;
2868
+ this._returnFocus = options.returnFocus instanceof HTMLElement ? options.returnFocus : input;
2640
2869
  }
2641
2870
  panel.showPopover();
2642
2871
  this._open = true;
2643
2872
  this._setExpanded(true);
2644
- const position = () => reposition(this, panel, { placement: "bottom-start", distance: 4, shiftPadding: 8 });
2873
+ const coordinateSpace = this.coordinateSpace;
2874
+ panel.style.position = coordinateSpace === "document" ? "absolute" : "fixed";
2875
+ const position = () => reposition(this, panel, {
2876
+ placement: "bottom-start",
2877
+ distance: 4,
2878
+ shiftPadding: 8,
2879
+ coordinateSpace
2880
+ });
2645
2881
  position();
2646
2882
  this._stopTracking = autoUpdate(this, panel, position);
2647
2883
  if (options.moveFocus !== false)
@@ -2651,7 +2887,8 @@
2651
2887
  _setExpanded(expanded) {
2652
2888
  const value = expanded ? "true" : "false";
2653
2889
  this._input?.setAttribute("aria-expanded", value);
2654
- this._button?.setAttribute("aria-expanded", value);
2890
+ for (const { button } of this._buttons)
2891
+ button.setAttribute("aria-expanded", value);
2655
2892
  for (const field of this._fields || [])
2656
2893
  field.input.setAttribute("aria-expanded", value);
2657
2894
  }
@@ -2673,7 +2910,7 @@
2673
2910
  if (endpoint)
2674
2911
  this._focusField(endpoint);
2675
2912
  } else {
2676
- this._focusInput();
2913
+ this._restoreFocus();
2677
2914
  }
2678
2915
  }
2679
2916
  this.dispatchEvent(new Event("close", { bubbles: true }));