@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.
- package/README.md +33 -8
- package/custom-elements.json +54 -0
- package/dist/date-picker.css +223 -76
- package/dist/date-picker.js +327 -90
- package/dist/date-picker.min.css +1 -1
- package/dist/date-picker.min.js +13 -13
- package/dist/date-picker.standalone.min.js +237 -90
- package/dist/types/date-calendar.d.ts +3 -0
- package/dist/types/date-calendar.d.ts.map +1 -1
- package/dist/types/date-field.d.ts +3 -1
- package/dist/types/date-field.d.ts.map +1 -1
- package/dist/types/date-picker.d.ts +86 -5
- package/dist/types/date-picker.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/intl.d.ts +9 -0
- package/dist/types/intl.d.ts.map +1 -1
- package/dist/types/time.d.ts +8 -0
- package/dist/types/time.d.ts.map +1 -0
- package/docs/ACCESSIBILITY.md +17 -9
- package/docs/API.md +68 -2
- package/docs/ARCHITECTURE.md +16 -2
- package/docs/AT-CHECKLIST.md +1 -1
- package/docs/DECISIONS.md +10 -1
- package/docs/MIGRATION-FLATPICKR.md +1 -1
- package/docs/ROADMAP.md +3 -2
- package/docs/USE_CASES.md +14 -1
- package/package.json +5 -5
- package/src/date-calendar.js +45 -13
- package/src/date-field.js +12 -4
- package/src/date-picker.css +223 -76
- package/src/date-picker.js +314 -80
- package/src/index.js +1 -0
- package/src/intl.js +35 -0
- package/src/time.js +39 -0
package/dist/date-picker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*** @lekoala/date-picker v0.
|
|
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-
|
|
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,
|
|
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
|
-
|
|
1162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1520
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
-
|
|
2009
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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.
|
|
2073
|
-
this._originalDescribedBy =
|
|
2217
|
+
if (!ranges && this._input) {
|
|
2218
|
+
this._originalDescribedBy = this._input.getAttribute("aria-describedby") || "";
|
|
2074
2219
|
const describedBy = [this._originalDescribedBy, hintId].filter(Boolean).join(" ");
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
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.
|
|
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.
|
|
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 || !
|
|
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
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
}
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
2552
|
+
single.setAttribute("aria-label", `${prefix}${suffix}`);
|
|
2355
2553
|
}
|
|
2356
2554
|
_refreshRangeButtonLabel() {
|
|
2357
|
-
if (!this.
|
|
2555
|
+
if (!this._range)
|
|
2358
2556
|
return;
|
|
2359
2557
|
const { start, end } = this._range;
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2843
|
+
} else {
|
|
2844
|
+
await this._commitText(false, { force: true });
|
|
2612
2845
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
2913
|
+
this._restoreFocus();
|
|
2677
2914
|
}
|
|
2678
2915
|
}
|
|
2679
2916
|
this.dispatchEvent(new Event("close", { bubbles: true }));
|