@vanduo-oss/framework 1.3.5 → 1.3.7

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
- /*! Vanduo v1.3.5 | Built: 2026-04-15T18:39:53.955Z | git:3ca4f62 | development */
1
+ /*! Vanduo v1.3.7 | Built: 2026-04-18T12:05:32.603Z | git:20b2d08 | development */
2
2
 
3
3
  // js/utils/lifecycle.js
4
4
  (function() {
@@ -107,7 +107,7 @@
107
107
  // js/vanduo.js
108
108
  (function() {
109
109
  "use strict";
110
- const VANDUO_VERSION = true ? "1.3.5" : "0.0.0-dev";
110
+ const VANDUO_VERSION = true ? "1.3.7" : "0.0.0-dev";
111
111
  const Vanduo2 = {
112
112
  version: VANDUO_VERSION,
113
113
  components: {},
@@ -6669,10 +6669,6 @@
6669
6669
  next.classList.remove("vd-morph-next");
6670
6670
  next.classList.add("vd-morph-current");
6671
6671
  }
6672
- el.classList.add("morph-done");
6673
- setTimeout(function() {
6674
- el.classList.remove("morph-done");
6675
- }, 350);
6676
6672
  if (typeof onComplete === "function") onComplete();
6677
6673
  }, duration);
6678
6674
  }
@@ -6683,6 +6679,303 @@
6683
6679
  window.VanduoMorph = Morph;
6684
6680
  })();
6685
6681
 
6682
+ // js/components/expanding-cards.js
6683
+ (function() {
6684
+ "use strict";
6685
+ const ExpandingCards = {
6686
+ instances: /* @__PURE__ */ new Map(),
6687
+ init: function() {
6688
+ document.querySelectorAll(".vd-expanding-cards").forEach(function(el) {
6689
+ if (el.getAttribute("data-vd-expanding-cards") === "manual") return;
6690
+ if (ExpandingCards.instances.has(el)) return;
6691
+ ExpandingCards.initContainer(el);
6692
+ });
6693
+ },
6694
+ initContainer: function(container) {
6695
+ const cleanup = [];
6696
+ const getCards = function() {
6697
+ return Array.prototype.slice.call(container.querySelectorAll(".vd-expanding-card"));
6698
+ };
6699
+ const setActive = function(card) {
6700
+ const cards = getCards();
6701
+ if (!card || cards.indexOf(card) === -1) return;
6702
+ cards.forEach(function(c) {
6703
+ c.classList.toggle("is-active", c === card);
6704
+ });
6705
+ card.focus({ preventScroll: true });
6706
+ };
6707
+ const onClick = function(e) {
6708
+ const t = e.target;
6709
+ const card = t.closest ? t.closest(".vd-expanding-card") : null;
6710
+ if (!card || !container.contains(card)) return;
6711
+ setActive(card);
6712
+ };
6713
+ const onKeydown = function(e) {
6714
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight" && e.key !== "Home" && e.key !== "End") {
6715
+ return;
6716
+ }
6717
+ const cards = getCards().filter(function(c) {
6718
+ return c.offsetParent !== null || c.getClientRects().length > 0;
6719
+ });
6720
+ if (!cards.length) return;
6721
+ const activeEl = document.activeElement;
6722
+ let idx = cards.indexOf(activeEl);
6723
+ if (idx < 0) {
6724
+ idx = cards.findIndex(function(c) {
6725
+ return c.classList.contains("is-active");
6726
+ });
6727
+ }
6728
+ if (idx < 0) idx = 0;
6729
+ if (e.key === "ArrowLeft") {
6730
+ e.preventDefault();
6731
+ setActive(cards[Math.max(0, idx - 1)]);
6732
+ } else if (e.key === "ArrowRight") {
6733
+ e.preventDefault();
6734
+ setActive(cards[Math.min(cards.length - 1, idx + 1)]);
6735
+ } else if (e.key === "Home") {
6736
+ e.preventDefault();
6737
+ setActive(cards[0]);
6738
+ } else if (e.key === "End") {
6739
+ e.preventDefault();
6740
+ setActive(cards[cards.length - 1]);
6741
+ }
6742
+ };
6743
+ container.addEventListener("click", onClick);
6744
+ cleanup.push(function() {
6745
+ container.removeEventListener("click", onClick);
6746
+ });
6747
+ container.addEventListener("keydown", onKeydown);
6748
+ cleanup.push(function() {
6749
+ container.removeEventListener("keydown", onKeydown);
6750
+ });
6751
+ getCards().forEach(function(card) {
6752
+ if (!card.hasAttribute("tabindex")) {
6753
+ card.setAttribute("tabindex", "0");
6754
+ }
6755
+ card.setAttribute("role", "button");
6756
+ if (!card.hasAttribute("aria-pressed")) {
6757
+ card.setAttribute("aria-pressed", card.classList.contains("is-active") ? "true" : "false");
6758
+ }
6759
+ });
6760
+ const syncAria = function() {
6761
+ getCards().forEach(function(card) {
6762
+ card.setAttribute("aria-pressed", card.classList.contains("is-active") ? "true" : "false");
6763
+ });
6764
+ };
6765
+ const mo = new MutationObserver(syncAria);
6766
+ mo.observe(container, { attributes: true, subtree: true, attributeFilter: ["class"] });
6767
+ cleanup.push(function() {
6768
+ mo.disconnect();
6769
+ });
6770
+ syncAria();
6771
+ ExpandingCards.instances.set(container, { cleanup });
6772
+ },
6773
+ destroy: function(container) {
6774
+ const inst = this.instances.get(container);
6775
+ if (!inst) return;
6776
+ inst.cleanup.forEach(function(fn) {
6777
+ fn();
6778
+ });
6779
+ this.instances.delete(container);
6780
+ },
6781
+ destroyAll: function() {
6782
+ this.instances.forEach(function(_, el) {
6783
+ ExpandingCards.destroy(el);
6784
+ });
6785
+ }
6786
+ };
6787
+ if (typeof window.Vanduo !== "undefined") {
6788
+ window.Vanduo.register("expandingCards", ExpandingCards);
6789
+ }
6790
+ window.VanduoExpandingCards = ExpandingCards;
6791
+ })();
6792
+
6793
+ // js/components/timeline.js
6794
+ (function() {
6795
+ "use strict";
6796
+ const STAGGER_MS = 140;
6797
+ const MAX_STAGGER_INDEX = 7;
6798
+ const PLAY_INTERVAL_MS = 800;
6799
+ function countRevealedPrefix(items) {
6800
+ let count = 0;
6801
+ for (let i = 0; i < items.length; i++) {
6802
+ if (!items[i].classList.contains("is-revealed")) break;
6803
+ count++;
6804
+ }
6805
+ return count;
6806
+ }
6807
+ function findPlaybackControls(container) {
6808
+ return container.parentElement || document.body;
6809
+ }
6810
+ function initPlayback(container, items, cleanup) {
6811
+ items.forEach(function(item) {
6812
+ item.classList.remove("is-revealed");
6813
+ });
6814
+ const scope = findPlaybackControls(container);
6815
+ const prevBtn = scope.querySelector("[data-vd-timeline-prev]");
6816
+ const nextBtn = scope.querySelector("[data-vd-timeline-next]");
6817
+ const playBtn = scope.querySelector("[data-vd-timeline-play]");
6818
+ const pauseBtn = scope.querySelector("[data-vd-timeline-pause]");
6819
+ let playTimer = null;
6820
+ function updateNavButtons() {
6821
+ const k = countRevealedPrefix(items);
6822
+ const n = items.length;
6823
+ if (prevBtn) {
6824
+ const atStart = k === 0;
6825
+ prevBtn.disabled = atStart;
6826
+ prevBtn.setAttribute("aria-disabled", atStart ? "true" : "false");
6827
+ }
6828
+ if (nextBtn) {
6829
+ const atEnd = k >= n;
6830
+ nextBtn.disabled = atEnd;
6831
+ nextBtn.setAttribute("aria-disabled", atEnd ? "true" : "false");
6832
+ }
6833
+ if (playBtn) {
6834
+ playBtn.setAttribute("aria-pressed", playTimer ? "true" : "false");
6835
+ }
6836
+ if (pauseBtn) {
6837
+ pauseBtn.disabled = !playTimer;
6838
+ }
6839
+ }
6840
+ function stepNext() {
6841
+ const k = countRevealedPrefix(items);
6842
+ if (k < items.length) {
6843
+ items[k].classList.add("is-revealed");
6844
+ }
6845
+ updateNavButtons();
6846
+ }
6847
+ function stepPrev() {
6848
+ const k = countRevealedPrefix(items);
6849
+ if (k > 0) {
6850
+ items[k - 1].classList.remove("is-revealed");
6851
+ }
6852
+ updateNavButtons();
6853
+ }
6854
+ function play() {
6855
+ if (playTimer) return;
6856
+ playTimer = setInterval(function() {
6857
+ if (countRevealedPrefix(items) >= items.length) {
6858
+ pause();
6859
+ return;
6860
+ }
6861
+ stepNext();
6862
+ }, PLAY_INTERVAL_MS);
6863
+ updateNavButtons();
6864
+ }
6865
+ function pause() {
6866
+ if (playTimer) {
6867
+ clearInterval(playTimer);
6868
+ playTimer = null;
6869
+ }
6870
+ updateNavButtons();
6871
+ }
6872
+ function addClick(el, fn) {
6873
+ if (!el) return;
6874
+ const handler = function(e) {
6875
+ e.preventDefault();
6876
+ fn();
6877
+ };
6878
+ el.addEventListener("click", handler);
6879
+ cleanup.push(function() {
6880
+ el.removeEventListener("click", handler);
6881
+ });
6882
+ }
6883
+ addClick(prevBtn, stepPrev);
6884
+ addClick(nextBtn, stepNext);
6885
+ addClick(playBtn, play);
6886
+ addClick(pauseBtn, pause);
6887
+ cleanup.push(function() {
6888
+ pause();
6889
+ });
6890
+ updateNavButtons();
6891
+ return {
6892
+ stepNext,
6893
+ stepPrev,
6894
+ play,
6895
+ pause
6896
+ };
6897
+ }
6898
+ const Timeline = {
6899
+ instances: /* @__PURE__ */ new Map(),
6900
+ init: function() {
6901
+ document.querySelectorAll(".vd-timeline.vd-timeline-animated").forEach(function(el) {
6902
+ if (Timeline.instances.has(el)) return;
6903
+ Timeline.initInstance(el);
6904
+ });
6905
+ },
6906
+ reinit: function() {
6907
+ Timeline.destroyAll();
6908
+ Timeline.init();
6909
+ },
6910
+ initInstance: function(container) {
6911
+ const cleanup = [];
6912
+ const items = Array.prototype.filter.call(container.children, function(child) {
6913
+ return child.classList && child.classList.contains("vd-timeline-item");
6914
+ });
6915
+ items.forEach(function(item, i) {
6916
+ const idx = Math.min(i, MAX_STAGGER_INDEX);
6917
+ item.style.setProperty("--vd-timeline-reveal-delay", idx * STAGGER_MS + "ms");
6918
+ });
6919
+ const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
6920
+ if (reducedMotion) {
6921
+ items.forEach(function(item) {
6922
+ item.classList.add("is-revealed");
6923
+ });
6924
+ Timeline.instances.set(container, { cleanup });
6925
+ return;
6926
+ }
6927
+ const playback = container.classList && container.classList.contains("vd-timeline-playback");
6928
+ if (playback) {
6929
+ const playbackApi = initPlayback(container, items, cleanup);
6930
+ Timeline.instances.set(container, { cleanup, playback: playbackApi });
6931
+ return;
6932
+ }
6933
+ if (typeof IntersectionObserver === "undefined") {
6934
+ items.forEach(function(item) {
6935
+ item.classList.add("is-revealed");
6936
+ });
6937
+ Timeline.instances.set(container, { cleanup });
6938
+ return;
6939
+ }
6940
+ const observer = new IntersectionObserver(function(entries) {
6941
+ entries.forEach(function(entry) {
6942
+ if (!entry.isIntersecting) return;
6943
+ entry.target.classList.add("is-revealed");
6944
+ observer.unobserve(entry.target);
6945
+ });
6946
+ }, {
6947
+ root: null,
6948
+ rootMargin: "0px 0px -10% 0px",
6949
+ threshold: 0.15
6950
+ });
6951
+ items.forEach(function(item) {
6952
+ observer.observe(item);
6953
+ });
6954
+ cleanup.push(function() {
6955
+ observer.disconnect();
6956
+ });
6957
+ Timeline.instances.set(container, { cleanup });
6958
+ },
6959
+ destroy: function(container) {
6960
+ const inst = this.instances.get(container);
6961
+ if (!inst) return;
6962
+ inst.cleanup.forEach(function(fn) {
6963
+ fn();
6964
+ });
6965
+ this.instances.delete(container);
6966
+ },
6967
+ destroyAll: function() {
6968
+ this.instances.forEach(function(_, el) {
6969
+ Timeline.destroy(el);
6970
+ });
6971
+ }
6972
+ };
6973
+ if (typeof window.Vanduo !== "undefined") {
6974
+ window.Vanduo.register("timeline", Timeline);
6975
+ }
6976
+ window.VanduoTimeline = Timeline;
6977
+ })();
6978
+
6686
6979
  // js/components/flow.js
6687
6980
  (function() {
6688
6981
  "use strict";
@@ -7699,6 +7992,115 @@
7699
7992
  "use strict";
7700
7993
  const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
7701
7994
  const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
7995
+ function escapeRegexChar(c) {
7996
+ return c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7997
+ }
7998
+ function buildParseFormat(format) {
7999
+ let regex = "^";
8000
+ const order = [];
8001
+ let i = 0;
8002
+ while (i < format.length) {
8003
+ const slice = format.slice(i);
8004
+ if (slice.toLowerCase().startsWith("yyyy")) {
8005
+ regex += "(\\d{4})";
8006
+ order.push("y");
8007
+ i += 4;
8008
+ } else if (slice.toLowerCase().startsWith("mm")) {
8009
+ regex += "(\\d{2})";
8010
+ order.push("m");
8011
+ i += 2;
8012
+ } else if (slice.toLowerCase().startsWith("dd")) {
8013
+ regex += "(\\d{2})";
8014
+ order.push("d");
8015
+ i += 2;
8016
+ } else {
8017
+ regex += escapeRegexChar(format[i]);
8018
+ i++;
8019
+ }
8020
+ }
8021
+ regex += "$";
8022
+ return { regex: new RegExp(regex), order };
8023
+ }
8024
+ function parseDateFromFormat(value, format) {
8025
+ if (!value || !format) return null;
8026
+ const { regex, order } = buildParseFormat(format);
8027
+ const m = value.trim().match(regex);
8028
+ if (!m) return null;
8029
+ let y;
8030
+ let mo;
8031
+ let d;
8032
+ let ci = 1;
8033
+ for (let k = 0; k < order.length; k++) {
8034
+ const part = order[k];
8035
+ const v = parseInt(m[ci++], 10);
8036
+ if (Number.isNaN(v)) return null;
8037
+ if (part === "y") y = v;
8038
+ else if (part === "m") mo = v - 1;
8039
+ else if (part === "d") d = v;
8040
+ }
8041
+ if (y === void 0 || mo === void 0 || d === void 0) return null;
8042
+ const dt = new Date(y, mo, d);
8043
+ if (dt.getFullYear() !== y || dt.getMonth() !== mo || dt.getDate() !== d) return null;
8044
+ return dt;
8045
+ }
8046
+ function formatDate(d, format) {
8047
+ const yyyy = String(d.getFullYear());
8048
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
8049
+ const dd = String(d.getDate()).padStart(2, "0");
8050
+ let out = "";
8051
+ let i = 0;
8052
+ while (i < format.length) {
8053
+ const slice = format.slice(i);
8054
+ if (slice.toLowerCase().startsWith("yyyy")) {
8055
+ out += yyyy;
8056
+ i += 4;
8057
+ } else if (slice.toLowerCase().startsWith("mm")) {
8058
+ out += mm;
8059
+ i += 2;
8060
+ } else if (slice.toLowerCase().startsWith("dd")) {
8061
+ out += dd;
8062
+ i += 2;
8063
+ } else {
8064
+ out += format[i];
8065
+ i++;
8066
+ }
8067
+ }
8068
+ return out;
8069
+ }
8070
+ function dateKey(d) {
8071
+ return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
8072
+ }
8073
+ function addDays(d, n) {
8074
+ const x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
8075
+ x.setDate(x.getDate() + n);
8076
+ return x;
8077
+ }
8078
+ function addMonthsClamped(d, n) {
8079
+ return new Date(d.getFullYear(), d.getMonth() + n, d.getDate());
8080
+ }
8081
+ function parseYmdLocal(ymd) {
8082
+ if (!ymd || typeof ymd !== "string") return null;
8083
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd.trim());
8084
+ if (!m) return null;
8085
+ const y = +m[1];
8086
+ const mo = +m[2] - 1;
8087
+ const day = +m[3];
8088
+ const dt = new Date(y, mo, day);
8089
+ if (dt.getFullYear() !== y || dt.getMonth() !== mo || dt.getDate() !== day) return null;
8090
+ return dt;
8091
+ }
8092
+ function startOfWeekSunday(d) {
8093
+ const x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
8094
+ const day = x.getDay();
8095
+ x.setDate(x.getDate() - day);
8096
+ return x;
8097
+ }
8098
+ function endOfWeekSunday(d) {
8099
+ const x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
8100
+ const day = x.getDay();
8101
+ x.setDate(x.getDate() + (6 - day));
8102
+ return x;
8103
+ }
7702
8104
  const Datepicker = {
7703
8105
  instances: /* @__PURE__ */ new Map(),
7704
8106
  init: function() {
@@ -7710,28 +8112,70 @@
7710
8112
  },
7711
8113
  initInstance: function(input) {
7712
8114
  const cleanup = [];
7713
- const format = input.getAttribute("data-vd-datepicker-format") || "yyyy-mm-dd";
8115
+ const format = input.getAttribute("data-vd-datepicker-format") || "YYYY-MM-DD";
7714
8116
  const minStr = input.getAttribute("data-vd-datepicker-min");
7715
8117
  const maxStr = input.getAttribute("data-vd-datepicker-max");
7716
- const minDate = minStr ? new Date(minStr) : null;
7717
- const maxDate = maxStr ? new Date(maxStr) : null;
8118
+ const minDate = minStr ? parseYmdLocal(minStr) : null;
8119
+ const maxDate = maxStr ? parseYmdLocal(maxStr) : null;
7718
8120
  const today = /* @__PURE__ */ new Date();
7719
8121
  let viewYear = today.getFullYear();
7720
8122
  let viewMonth = today.getMonth();
7721
8123
  let selectedDate = null;
7722
8124
  let viewMode = "days";
8125
+ let focusedDate = null;
8126
+ let skipNextFocusOpen = false;
8127
+ const isDisabled = (d) => {
8128
+ if (minDate) {
8129
+ const t = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
8130
+ if (t < minDate.getTime()) return true;
8131
+ }
8132
+ if (maxDate) {
8133
+ const t = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
8134
+ if (t > maxDate.getTime()) return true;
8135
+ }
8136
+ return false;
8137
+ };
8138
+ const ensureMonthInRange = (y, m) => {
8139
+ if (!minDate && !maxDate) return { y, m };
8140
+ const first = new Date(y, m, 1);
8141
+ const last = new Date(y, m + 1, 0);
8142
+ if (minDate && last.getTime() < minDate.getTime()) {
8143
+ return { y: minDate.getFullYear(), m: minDate.getMonth() };
8144
+ }
8145
+ if (maxDate && first.getTime() > maxDate.getTime()) {
8146
+ return { y: maxDate.getFullYear(), m: maxDate.getMonth() };
8147
+ }
8148
+ return { y, m };
8149
+ };
8150
+ const firstSelectableInMonth = (y, m) => {
8151
+ const last = new Date(y, m + 1, 0).getDate();
8152
+ for (let day = 1; day <= last; day++) {
8153
+ const dt = new Date(y, m, day);
8154
+ if (!isDisabled(dt)) return dt;
8155
+ }
8156
+ return new Date(y, m, 1);
8157
+ };
7723
8158
  if (input.value) {
7724
- const parsed = new Date(input.value);
7725
- if (!isNaN(parsed.getTime())) {
8159
+ const trimmed = input.value.trim();
8160
+ let parsed = parseDateFromFormat(trimmed, format);
8161
+ if (!parsed) {
8162
+ const fallback = new Date(trimmed);
8163
+ if (!isNaN(fallback.getTime())) parsed = fallback;
8164
+ }
8165
+ if (parsed) {
7726
8166
  selectedDate = parsed;
7727
8167
  viewYear = parsed.getFullYear();
7728
8168
  viewMonth = parsed.getMonth();
7729
8169
  }
7730
8170
  }
8171
+ const clampedInit = ensureMonthInRange(viewYear, viewMonth);
8172
+ viewYear = clampedInit.y;
8173
+ viewMonth = clampedInit.m;
7731
8174
  const popup = document.createElement("div");
7732
8175
  popup.className = "vd-datepicker-popup";
7733
8176
  popup.setAttribute("role", "dialog");
7734
8177
  popup.setAttribute("aria-label", "Choose date");
8178
+ popup.tabIndex = -1;
7735
8179
  const wrapper = document.createElement("div");
7736
8180
  wrapper.className = "vd-suggest-wrapper";
7737
8181
  wrapper.style.position = "relative";
@@ -7739,18 +8183,80 @@
7739
8183
  input.parentNode.insertBefore(wrapper, input);
7740
8184
  wrapper.appendChild(input);
7741
8185
  wrapper.appendChild(popup);
7742
- const formatDate = (d) => {
7743
- const yyyy = d.getFullYear();
7744
- const mm = String(d.getMonth() + 1).padStart(2, "0");
7745
- const dd = String(d.getDate()).padStart(2, "0");
7746
- return format.replace("yyyy", yyyy).replace("mm", mm).replace("dd", dd);
8186
+ const isSameDay = (a, b) => a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
8187
+ const selectDate = (date) => {
8188
+ selectedDate = date;
8189
+ viewYear = date.getFullYear();
8190
+ viewMonth = date.getMonth();
8191
+ input.value = formatDate(date, format);
8192
+ skipNextFocusOpen = true;
8193
+ close();
8194
+ input.dispatchEvent(new CustomEvent("datepicker:select", {
8195
+ detail: { date, formatted: input.value },
8196
+ bubbles: true
8197
+ }));
8198
+ input.dispatchEvent(new Event("change", { bubbles: true }));
8199
+ input.focus();
7747
8200
  };
7748
- const isDisabled = (d) => {
7749
- if (minDate && d < minDate) return true;
7750
- if (maxDate && d > maxDate) return true;
7751
- return false;
8201
+ const focusFocusedDay = () => {
8202
+ if (viewMode !== "days" || !focusedDate) return;
8203
+ const key = dateKey(focusedDate);
8204
+ const btn = popup.querySelector('[data-vd-date="' + key + '"]');
8205
+ if (btn && !btn.classList.contains("is-outside") && btn.getAttribute("aria-disabled") !== "true") {
8206
+ btn.focus();
8207
+ }
8208
+ };
8209
+ const skipDisabled = (d, stepDir, maxSteps) => {
8210
+ let x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
8211
+ const step = stepDir > 0 ? 1 : -1;
8212
+ for (let i = 0; i < maxSteps; i++) {
8213
+ if (!isDisabled(x)) return x;
8214
+ x = addDays(x, step);
8215
+ }
8216
+ return d;
8217
+ };
8218
+ const createDayBtn = (day, outside, date) => {
8219
+ const btn = document.createElement("button");
8220
+ btn.type = "button";
8221
+ btn.className = "vd-datepicker-day";
8222
+ btn.textContent = day;
8223
+ btn.setAttribute("role", "gridcell");
8224
+ if (outside) {
8225
+ btn.classList.add("is-outside");
8226
+ btn.tabIndex = -1;
8227
+ btn.setAttribute("aria-disabled", "true");
8228
+ return btn;
8229
+ }
8230
+ btn.setAttribute("data-vd-date", dateKey(date));
8231
+ if (date && isSameDay(date, today)) btn.classList.add("is-today");
8232
+ if (date && isSameDay(date, selectedDate)) btn.classList.add("is-selected");
8233
+ if (date && isDisabled(date)) {
8234
+ btn.classList.add("is-disabled");
8235
+ btn.setAttribute("aria-disabled", "true");
8236
+ btn.tabIndex = -1;
8237
+ return btn;
8238
+ }
8239
+ if (date) {
8240
+ const isFocused = focusedDate && isSameDay(date, focusedDate);
8241
+ btn.tabIndex = isFocused ? 0 : -1;
8242
+ btn.addEventListener("click", () => {
8243
+ selectedDate = date;
8244
+ viewYear = date.getFullYear();
8245
+ viewMonth = date.getMonth();
8246
+ focusedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
8247
+ input.value = formatDate(date, format);
8248
+ skipNextFocusOpen = true;
8249
+ close();
8250
+ input.dispatchEvent(new CustomEvent("datepicker:select", {
8251
+ detail: { date, formatted: input.value },
8252
+ bubbles: true
8253
+ }));
8254
+ input.dispatchEvent(new Event("change", { bubbles: true }));
8255
+ input.focus();
8256
+ });
8257
+ }
8258
+ return btn;
7752
8259
  };
7753
- const isSameDay = (a, b) => a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
7754
8260
  const render = () => {
7755
8261
  popup.innerHTML = "";
7756
8262
  const header = document.createElement("div");
@@ -7820,35 +8326,53 @@
7820
8326
  header.appendChild(nextBtn);
7821
8327
  popup.appendChild(header);
7822
8328
  if (viewMode === "days") {
8329
+ const gridWrap = document.createElement("div");
8330
+ gridWrap.className = "vd-datepicker-grid";
8331
+ gridWrap.setAttribute("role", "grid");
8332
+ gridWrap.setAttribute("aria-label", "Calendar");
7823
8333
  const weekdays = document.createElement("div");
7824
8334
  weekdays.className = "vd-datepicker-weekdays";
7825
- DAYS.forEach((d) => {
8335
+ weekdays.setAttribute("role", "row");
8336
+ DAYS.forEach(function(d) {
7826
8337
  const span = document.createElement("span");
8338
+ span.setAttribute("role", "columnheader");
8339
+ span.setAttribute("aria-label", d);
7827
8340
  span.textContent = d;
7828
8341
  weekdays.appendChild(span);
7829
8342
  });
7830
- popup.appendChild(weekdays);
7831
- const grid = document.createElement("div");
7832
- grid.className = "vd-datepicker-days";
8343
+ gridWrap.appendChild(weekdays);
7833
8344
  const firstDay = new Date(viewYear, viewMonth, 1).getDay();
7834
8345
  const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
7835
8346
  const daysInPrev = new Date(viewYear, viewMonth, 0).getDate();
8347
+ const cells = [];
7836
8348
  for (let i = firstDay - 1; i >= 0; i--) {
7837
- const btn = createDayBtn(daysInPrev - i, true);
7838
- grid.appendChild(btn);
8349
+ const dayNum = daysInPrev - i;
8350
+ const prevMonth = viewMonth === 0 ? 11 : viewMonth - 1;
8351
+ const prevYear = viewMonth === 0 ? viewYear - 1 : viewYear;
8352
+ const date = new Date(prevYear, prevMonth, dayNum);
8353
+ cells.push({ day: dayNum, outside: true, date });
7839
8354
  }
7840
8355
  for (let d = 1; d <= daysInMonth; d++) {
7841
8356
  const date = new Date(viewYear, viewMonth, d);
7842
- const btn = createDayBtn(d, false, date);
7843
- grid.appendChild(btn);
8357
+ cells.push({ day: d, outside: false, date });
7844
8358
  }
7845
8359
  const totalCells = firstDay + daysInMonth;
7846
8360
  const remaining = totalCells % 7 === 0 ? 0 : 7 - totalCells % 7;
7847
8361
  for (let i = 1; i <= remaining; i++) {
7848
- const btn = createDayBtn(i, true);
7849
- grid.appendChild(btn);
8362
+ const date = new Date(viewYear, viewMonth + 1, i);
8363
+ cells.push({ day: i, outside: true, date });
7850
8364
  }
7851
- popup.appendChild(grid);
8365
+ for (let r = 0; r < cells.length; r += 7) {
8366
+ const row = document.createElement("div");
8367
+ row.className = "vd-datepicker-row";
8368
+ row.setAttribute("role", "row");
8369
+ for (let c = 0; c < 7; c++) {
8370
+ const cell = cells[r + c];
8371
+ row.appendChild(createDayBtn(cell.day, cell.outside, cell.date));
8372
+ }
8373
+ gridWrap.appendChild(row);
8374
+ }
8375
+ popup.appendChild(gridWrap);
7852
8376
  } else if (viewMode === "months") {
7853
8377
  const grid = document.createElement("div");
7854
8378
  grid.className = "vd-datepicker-months";
@@ -7889,65 +8413,125 @@
7889
8413
  popup.appendChild(grid);
7890
8414
  }
7891
8415
  };
7892
- const createDayBtn = (day, outside, date) => {
7893
- const btn = document.createElement("button");
7894
- btn.type = "button";
7895
- btn.className = "vd-datepicker-day";
7896
- btn.textContent = day;
7897
- if (outside) {
7898
- btn.classList.add("is-outside");
7899
- btn.tabIndex = -1;
7900
- return btn;
8416
+ const handleGridKeydown = (e) => {
8417
+ if (!popup.classList.contains("is-open") || viewMode !== "days") return;
8418
+ const grid = popup.querySelector(".vd-datepicker-grid");
8419
+ if (!grid || !grid.contains(e.target)) return;
8420
+ const key = e.key;
8421
+ if (key !== "ArrowLeft" && key !== "ArrowRight" && key !== "ArrowUp" && key !== "ArrowDown" && key !== "Home" && key !== "End" && key !== "PageUp" && key !== "PageDown" && key !== "Enter" && key !== " " && key !== "Escape") {
8422
+ return;
7901
8423
  }
7902
- if (date && isSameDay(date, today)) btn.classList.add("is-today");
7903
- if (date && isSameDay(date, selectedDate)) btn.classList.add("is-selected");
7904
- if (date && isDisabled(date)) {
7905
- btn.classList.add("is-disabled");
7906
- return btn;
8424
+ if (key === "Escape") {
8425
+ e.preventDefault();
8426
+ e.stopPropagation();
8427
+ skipNextFocusOpen = true;
8428
+ close();
8429
+ input.focus();
8430
+ return;
7907
8431
  }
7908
- if (date) {
7909
- btn.addEventListener("click", () => {
7910
- selectedDate = date;
7911
- viewYear = date.getFullYear();
7912
- viewMonth = date.getMonth();
7913
- input.value = formatDate(date);
7914
- close();
7915
- input.dispatchEvent(new CustomEvent("datepicker:select", {
7916
- detail: { date, formatted: input.value },
7917
- bubbles: true
7918
- }));
7919
- input.dispatchEvent(new Event("change", { bubbles: true }));
7920
- });
8432
+ if (!focusedDate) {
8433
+ focusedDate = firstSelectableInMonth(viewYear, viewMonth);
7921
8434
  }
7922
- return btn;
8435
+ if (key === "Enter" || key === " ") {
8436
+ e.preventDefault();
8437
+ if (focusedDate && !isDisabled(focusedDate)) {
8438
+ selectDate(new Date(focusedDate.getFullYear(), focusedDate.getMonth(), focusedDate.getDate()));
8439
+ }
8440
+ return;
8441
+ }
8442
+ e.preventDefault();
8443
+ let next = new Date(focusedDate.getFullYear(), focusedDate.getMonth(), focusedDate.getDate());
8444
+ let skipDir = 1;
8445
+ if (key === "ArrowLeft") {
8446
+ next = addDays(next, -1);
8447
+ skipDir = -1;
8448
+ } else if (key === "ArrowRight") {
8449
+ next = addDays(next, 1);
8450
+ skipDir = 1;
8451
+ } else if (key === "ArrowUp") {
8452
+ next = addDays(next, -7);
8453
+ skipDir = -1;
8454
+ } else if (key === "ArrowDown") {
8455
+ next = addDays(next, 7);
8456
+ skipDir = 1;
8457
+ } else if (key === "Home") {
8458
+ next = startOfWeekSunday(next);
8459
+ skipDir = 1;
8460
+ } else if (key === "End") {
8461
+ next = endOfWeekSunday(next);
8462
+ skipDir = -1;
8463
+ } else if (key === "PageUp") {
8464
+ next = addMonthsClamped(next, -1);
8465
+ skipDir = -1;
8466
+ } else if (key === "PageDown") {
8467
+ next = addMonthsClamped(next, 1);
8468
+ skipDir = 1;
8469
+ }
8470
+ next = skipDisabled(next, skipDir, 400);
8471
+ if (next.getMonth() !== viewMonth || next.getFullYear() !== viewYear) {
8472
+ viewYear = next.getFullYear();
8473
+ viewMonth = next.getMonth();
8474
+ const cl = ensureMonthInRange(viewYear, viewMonth);
8475
+ viewYear = cl.y;
8476
+ viewMonth = cl.m;
8477
+ }
8478
+ focusedDate = next;
8479
+ render();
8480
+ requestAnimationFrame(focusFocusedDay);
7923
8481
  };
7924
8482
  const open = () => {
8483
+ viewMode = "days";
8484
+ if (selectedDate) {
8485
+ viewYear = selectedDate.getFullYear();
8486
+ viewMonth = selectedDate.getMonth();
8487
+ }
8488
+ const cl = ensureMonthInRange(viewYear, viewMonth);
8489
+ viewYear = cl.y;
8490
+ viewMonth = cl.m;
8491
+ if (selectedDate) {
8492
+ focusedDate = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate());
8493
+ } else {
8494
+ focusedDate = firstSelectableInMonth(viewYear, viewMonth);
8495
+ }
7925
8496
  render();
7926
8497
  popup.classList.add("is-open");
7927
8498
  input.setAttribute("aria-expanded", "true");
8499
+ requestAnimationFrame(focusFocusedDay);
7928
8500
  };
7929
8501
  const close = () => {
7930
8502
  popup.classList.remove("is-open");
7931
8503
  input.setAttribute("aria-expanded", "false");
7932
8504
  viewMode = "days";
7933
8505
  };
7934
- const focusHandler = () => open();
8506
+ const focusHandler = () => {
8507
+ if (skipNextFocusOpen) {
8508
+ skipNextFocusOpen = false;
8509
+ return;
8510
+ }
8511
+ open();
8512
+ };
7935
8513
  const outsideHandler = (e) => {
7936
8514
  if (!wrapper.contains(e.target)) close();
7937
8515
  };
7938
8516
  const escHandler = (e) => {
7939
- if (e.key === "Escape") close();
8517
+ if (e.key === "Escape" && popup.classList.contains("is-open")) {
8518
+ skipNextFocusOpen = true;
8519
+ close();
8520
+ input.focus();
8521
+ }
7940
8522
  };
7941
8523
  input.addEventListener("focus", focusHandler);
7942
8524
  document.addEventListener("click", outsideHandler, true);
7943
8525
  document.addEventListener("keydown", escHandler);
8526
+ popup.addEventListener("keydown", handleGridKeydown);
7944
8527
  input.setAttribute("aria-haspopup", "dialog");
7945
8528
  input.setAttribute("aria-expanded", "false");
7946
8529
  input.setAttribute("autocomplete", "off");
7947
8530
  cleanup.push(
7948
8531
  () => input.removeEventListener("focus", focusHandler),
7949
8532
  () => document.removeEventListener("click", outsideHandler, true),
7950
- () => document.removeEventListener("keydown", escHandler)
8533
+ () => document.removeEventListener("keydown", escHandler),
8534
+ () => popup.removeEventListener("keydown", handleGridKeydown)
7951
8535
  );
7952
8536
  this.instances.set(input, { cleanup, open, close, popup });
7953
8537
  },