@kalyx/react 1.0.0-rc.3 → 1.0.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -685,6 +685,86 @@ function DatePickerCalendar({
685
685
  /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", style: srOnly, children: announcement })
686
686
  ] });
687
687
  }
688
+ function isRangeFullyDisabled(start, end, rules, adapter) {
689
+ for (const rule of rules) {
690
+ if ("before" in rule && adapter.isBefore(end, rule.before)) return true;
691
+ if ("after" in rule && adapter.isAfter(start, rule.after)) return true;
692
+ }
693
+ return false;
694
+ }
695
+ function useGridState(opts) {
696
+ const { initialIndex, disabledFlags, onSelect, onPageUp, onPageDown, onEscape } = opts;
697
+ const gridRef = useRef(null);
698
+ const [focusedIndex, setFocusedIndex] = useState(initialIndex);
699
+ const handleKeyDown = (e) => {
700
+ let next = null;
701
+ let step = 1;
702
+ switch (e.key) {
703
+ case "ArrowLeft":
704
+ next = Math.max(0, focusedIndex - 1);
705
+ step = -1;
706
+ break;
707
+ case "ArrowRight":
708
+ next = Math.min(11, focusedIndex + 1);
709
+ break;
710
+ case "ArrowUp":
711
+ next = Math.max(0, focusedIndex - 3);
712
+ step = -1;
713
+ break;
714
+ case "ArrowDown":
715
+ next = Math.min(11, focusedIndex + 3);
716
+ break;
717
+ case "Home":
718
+ next = focusedIndex - focusedIndex % 3;
719
+ step = -1;
720
+ break;
721
+ case "End":
722
+ next = focusedIndex - focusedIndex % 3 + 2;
723
+ break;
724
+ case "PageUp":
725
+ e.preventDefault();
726
+ onPageUp();
727
+ return;
728
+ case "PageDown":
729
+ e.preventDefault();
730
+ onPageDown();
731
+ return;
732
+ case "Enter":
733
+ case " ":
734
+ e.preventDefault();
735
+ onSelect(focusedIndex);
736
+ return;
737
+ case "Escape":
738
+ onEscape();
739
+ return;
740
+ default:
741
+ return;
742
+ }
743
+ if (next === null) return;
744
+ e.preventDefault();
745
+ if (disabledFlags) {
746
+ let attempts = 0;
747
+ while (next >= 0 && next < 12 && disabledFlags[next] && attempts < 12) {
748
+ next += step;
749
+ attempts++;
750
+ }
751
+ if (next < 0 || next >= 12 || disabledFlags[next]) return;
752
+ }
753
+ if (next !== focusedIndex) setFocusedIndex(next);
754
+ };
755
+ useEffect(() => {
756
+ if (!disabledFlags || !disabledFlags[focusedIndex]) return;
757
+ const firstEnabled = disabledFlags.findIndex((d) => !d);
758
+ if (firstEnabled !== -1 && firstEnabled !== focusedIndex) {
759
+ setFocusedIndex(firstEnabled);
760
+ }
761
+ }, [disabledFlags, focusedIndex]);
762
+ useEffect(() => {
763
+ const btn = gridRef.current?.querySelector('[data-focused="true"]');
764
+ btn?.focus({ preventScroll: true });
765
+ }, [focusedIndex]);
766
+ return { gridRef, focusedIndex, handleKeyDown };
767
+ }
688
768
  function DatePickerMonthGrid({
689
769
  classNames,
690
770
  onSelect,
@@ -692,15 +772,18 @@ function DatePickerMonthGrid({
692
772
  ...props
693
773
  }) {
694
774
  const ctx = useDatePickerContext("DatePicker.MonthGrid");
695
- const { adapter, viewMonth, locale } = ctx;
775
+ const { adapter, viewMonth, locale, displayTimezone } = ctx;
696
776
  const currentYear = adapter.getYear(viewMonth);
697
777
  const currentMonth = adapter.getMonth(viewMonth);
698
- const todayMonth = adapter.getMonth(adapter.today());
699
- const todayYear = adapter.getYear(adapter.today());
778
+ const [today, setToday] = useState(null);
779
+ useEffect(() => {
780
+ setToday(adapter.today(displayTimezone));
781
+ }, [adapter, displayTimezone]);
782
+ const todayMonth = today !== null ? adapter.getMonth(today) : -1;
783
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
700
784
  const navigateYear = useCallback(
701
785
  (direction) => {
702
- const newDate = adapter.addYears(viewMonth, direction);
703
- ctx.setViewMonth(newDate);
786
+ ctx.setViewMonth(adapter.addYears(viewMonth, direction));
704
787
  },
705
788
  [adapter, viewMonth, ctx]
706
789
  );
@@ -713,12 +796,13 @@ function DatePickerMonthGrid({
713
796
  },
714
797
  [currentYear, ctx, onSelect]
715
798
  );
716
- const months = Array.from({ length: 12 }, (_, i) => ({
717
- index: i,
718
- name: getMonthName(i, locale),
719
- isSelected: i === currentMonth,
720
- isCurrent: i === todayMonth && currentYear === todayYear
721
- }));
799
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
800
+ initialIndex: currentMonth,
801
+ onSelect: handleMonthSelect,
802
+ onPageUp: () => navigateYear(-1),
803
+ onPageDown: () => navigateYear(1),
804
+ onEscape: ctx.close
805
+ });
722
806
  return /* @__PURE__ */ jsxs("div", { className: classNames?.root, ...props, children: [
723
807
  /* @__PURE__ */ jsxs("div", { className: classNames?.header, children: [
724
808
  /* @__PURE__ */ jsx(
@@ -746,30 +830,37 @@ function DatePickerMonthGrid({
746
830
  /* @__PURE__ */ jsx(
747
831
  "div",
748
832
  {
833
+ ref: gridRef,
749
834
  role: "grid",
750
835
  "aria-label": `${currentYear} months`,
751
836
  className: classNames?.grid,
752
837
  style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
753
- children: months.map((m) => {
754
- const monthClass = [
838
+ onKeyDown: handleKeyDown,
839
+ children: Array.from({ length: 12 }, (_, i) => {
840
+ const isSelected = i === currentMonth;
841
+ const isCurrent = i === todayMonth && currentYear === todayYear;
842
+ const isFocused = i === focusedIndex;
843
+ const cls = [
755
844
  classNames?.month,
756
- m.isSelected && classNames?.monthSelected,
757
- m.isCurrent && classNames?.monthCurrent
845
+ isSelected && classNames?.monthSelected,
846
+ isCurrent && classNames?.monthCurrent
758
847
  ].filter(Boolean).join(" ") || void 0;
759
848
  return /* @__PURE__ */ jsx(
760
849
  "button",
761
850
  {
762
851
  type: "button",
763
852
  role: "gridcell",
764
- "aria-selected": m.isSelected || void 0,
765
- "aria-current": m.isCurrent ? "date" : void 0,
766
- "data-selected": m.isSelected || void 0,
767
- "data-current": m.isCurrent || void 0,
768
- className: monthClass,
769
- onClick: () => handleMonthSelect(m.index),
770
- children: m.name
853
+ tabIndex: isFocused ? 0 : -1,
854
+ "aria-selected": isSelected || void 0,
855
+ "aria-current": isCurrent ? "date" : void 0,
856
+ "data-selected": isSelected || void 0,
857
+ "data-current": isCurrent || void 0,
858
+ "data-focused": isFocused || void 0,
859
+ className: cls,
860
+ onClick: () => handleMonthSelect(i),
861
+ children: getMonthName(i, locale)
771
862
  },
772
- m.index
863
+ i
773
864
  );
774
865
  })
775
866
  }
@@ -778,38 +869,38 @@ function DatePickerMonthGrid({
778
869
  }
779
870
  function DatePickerYearGrid({ classNames, onSelect, ...props }) {
780
871
  const ctx = useDatePickerContext("DatePicker.YearGrid");
781
- const { adapter, viewMonth } = ctx;
872
+ const { adapter, viewMonth, displayTimezone } = ctx;
782
873
  const currentYear = adapter.getYear(viewMonth);
783
- const todayYear = adapter.getYear(adapter.today());
874
+ const [today, setToday] = useState(null);
875
+ useEffect(() => {
876
+ setToday(adapter.today(displayTimezone));
877
+ }, [adapter, displayTimezone]);
878
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
784
879
  const decadeStart = currentYear - currentYear % 12;
785
880
  const navigateDecade = useCallback(
786
881
  (direction) => {
787
- const newDate = adapter.addYears(viewMonth, direction * 12);
788
- ctx.setViewMonth(newDate);
882
+ ctx.setViewMonth(adapter.addYears(viewMonth, direction * 12));
789
883
  },
790
884
  [adapter, viewMonth, ctx]
791
885
  );
792
886
  const handleYearSelect = useCallback(
793
- (year) => {
887
+ (indexInDecade) => {
888
+ const year = decadeStart + indexInDecade;
794
889
  const currentMonth = adapter.getMonth(viewMonth);
795
890
  const target = new Date(Date.UTC(year, currentMonth, 1)).toISOString();
796
891
  ctx.setViewMonth(target);
797
892
  ctx.setFocusedDate(target);
798
893
  onSelect?.();
799
894
  },
800
- [adapter, viewMonth, ctx, onSelect]
801
- );
802
- const years = useMemo(
803
- () => Array.from({ length: 12 }, (_, i) => {
804
- const year = decadeStart + i;
805
- return {
806
- value: year,
807
- isSelected: year === currentYear,
808
- isCurrent: year === todayYear
809
- };
810
- }),
811
- [decadeStart, currentYear, todayYear]
812
- );
895
+ [adapter, viewMonth, ctx, onSelect, decadeStart]
896
+ );
897
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
898
+ initialIndex: currentYear - decadeStart,
899
+ onSelect: handleYearSelect,
900
+ onPageUp: () => navigateDecade(-1),
901
+ onPageDown: () => navigateDecade(1),
902
+ onEscape: ctx.close
903
+ });
813
904
  const rangeLabel = `${decadeStart}\u2013${decadeStart + 11}`;
814
905
  return /* @__PURE__ */ jsxs("div", { className: classNames?.root, ...props, children: [
815
906
  /* @__PURE__ */ jsxs("div", { className: classNames?.header, children: [
@@ -838,30 +929,38 @@ function DatePickerYearGrid({ classNames, onSelect, ...props }) {
838
929
  /* @__PURE__ */ jsx(
839
930
  "div",
840
931
  {
932
+ ref: gridRef,
841
933
  role: "grid",
842
934
  "aria-label": rangeLabel,
843
935
  className: classNames?.grid,
844
936
  style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
845
- children: years.map((y) => {
846
- const yearClass = [
937
+ onKeyDown: handleKeyDown,
938
+ children: Array.from({ length: 12 }, (_, i) => {
939
+ const year = decadeStart + i;
940
+ const isSelected = year === currentYear;
941
+ const isCurrent = year === todayYear;
942
+ const isFocused = i === focusedIndex;
943
+ const cls = [
847
944
  classNames?.year,
848
- y.isSelected && classNames?.yearSelected,
849
- y.isCurrent && classNames?.yearCurrent
945
+ isSelected && classNames?.yearSelected,
946
+ isCurrent && classNames?.yearCurrent
850
947
  ].filter(Boolean).join(" ") || void 0;
851
948
  return /* @__PURE__ */ jsx(
852
949
  "button",
853
950
  {
854
951
  type: "button",
855
952
  role: "gridcell",
856
- "aria-selected": y.isSelected || void 0,
857
- "aria-current": y.isCurrent ? "date" : void 0,
858
- "data-selected": y.isSelected || void 0,
859
- "data-current": y.isCurrent || void 0,
860
- className: yearClass,
861
- onClick: () => handleYearSelect(y.value),
862
- children: y.value
953
+ tabIndex: isFocused ? 0 : -1,
954
+ "aria-selected": isSelected || void 0,
955
+ "aria-current": isCurrent ? "date" : void 0,
956
+ "data-selected": isSelected || void 0,
957
+ "data-current": isCurrent || void 0,
958
+ "data-focused": isFocused || void 0,
959
+ className: cls,
960
+ onClick: () => handleYearSelect(i),
961
+ children: year
863
962
  },
864
- y.value
963
+ i
865
964
  );
866
965
  })
867
966
  }
@@ -934,8 +1033,7 @@ function DatePickerPreset({
934
1033
  "button",
935
1034
  {
936
1035
  type: "button",
937
- role: "option",
938
- "aria-selected": isActive,
1036
+ "aria-pressed": isActive,
939
1037
  "data-active": isActive || void 0,
940
1038
  disabled: ctx.isDisabled,
941
1039
  onClick: handleClick,
@@ -1427,7 +1525,6 @@ function RangePickerCalendar({
1427
1525
  ref: gridRef,
1428
1526
  role: "grid",
1429
1527
  "aria-label": title,
1430
- "aria-multiselectable": "true",
1431
1528
  "aria-rowcount": weeks.length + 1,
1432
1529
  "aria-colcount": 7,
1433
1530
  className: classNames?.grid,
@@ -1591,8 +1688,7 @@ function RangePickerPreset({
1591
1688
  "button",
1592
1689
  {
1593
1690
  type: "button",
1594
- role: "option",
1595
- "aria-selected": isActive,
1691
+ "aria-pressed": isActive,
1596
1692
  "data-active": isActive || void 0,
1597
1693
  disabled: ctx.isDisabled,
1598
1694
  onClick: handleClick,
@@ -1896,29 +1992,70 @@ function TimePickerMinuteList({ classNames, ...props }) {
1896
1992
  }
1897
1993
  function TimePickerAmPmToggle({ classNames, ...props }) {
1898
1994
  const ctx = useTimePickerContext("TimePicker.AmPmToggle");
1899
- if (ctx.format !== "12h") return null;
1900
- const { period, hours12 } = to12Hour(ctx.currentTime.hours);
1995
+ const amRef = useRef(null);
1996
+ const pmRef = useRef(null);
1901
1997
  const setPeriod = useCallback(
1902
1998
  (newPeriod) => {
1903
1999
  if (ctx.isDisabled || ctx.isReadOnly) return;
2000
+ const { hours12 } = to12Hour(ctx.currentTime.hours);
1904
2001
  const newHours24 = to24Hour(hours12, newPeriod);
1905
2002
  ctx.setTime({ hours: newHours24 });
1906
2003
  },
1907
- [hours12, ctx]
2004
+ [ctx]
1908
2005
  );
2006
+ if (ctx.format !== "12h") return null;
2007
+ const { period } = to12Hour(ctx.currentTime.hours);
2008
+ const focusOther = (target) => {
2009
+ (target === "AM" ? amRef : pmRef).current?.focus();
2010
+ };
2011
+ const handleKeyDown = (e, target) => {
2012
+ switch (e.key) {
2013
+ case "ArrowRight":
2014
+ case "ArrowDown":
2015
+ case "ArrowLeft":
2016
+ case "ArrowUp": {
2017
+ e.preventDefault();
2018
+ const next = target === "AM" ? "PM" : "AM";
2019
+ setPeriod(next);
2020
+ focusOther(next);
2021
+ break;
2022
+ }
2023
+ case "Home": {
2024
+ e.preventDefault();
2025
+ setPeriod("AM");
2026
+ focusOther("AM");
2027
+ break;
2028
+ }
2029
+ case "End": {
2030
+ e.preventDefault();
2031
+ setPeriod("PM");
2032
+ focusOther("PM");
2033
+ break;
2034
+ }
2035
+ case " ":
2036
+ case "Enter": {
2037
+ e.preventDefault();
2038
+ setPeriod(target);
2039
+ break;
2040
+ }
2041
+ }
2042
+ };
1909
2043
  const renderButton = (target) => {
1910
2044
  const isSelected = period === target;
1911
2045
  const optionClass = [classNames?.option, isSelected && classNames?.optionSelected].filter(Boolean).join(" ") || void 0;
1912
2046
  return /* @__PURE__ */ jsx(
1913
2047
  "button",
1914
2048
  {
2049
+ ref: target === "AM" ? amRef : pmRef,
1915
2050
  type: "button",
1916
2051
  role: "radio",
1917
2052
  "aria-checked": isSelected,
2053
+ tabIndex: isSelected ? 0 : -1,
1918
2054
  "data-selected": isSelected || void 0,
1919
2055
  disabled: ctx.isDisabled,
1920
2056
  className: optionClass,
1921
2057
  onClick: () => setPeriod(target),
2058
+ onKeyDown: (e) => handleKeyDown(e, target),
1922
2059
  children: target
1923
2060
  }
1924
2061
  );
@@ -2200,7 +2337,7 @@ function MonthPickerRoot(props) {
2200
2337
  }
2201
2338
  function MonthPickerGrid({ classNames, ...props }) {
2202
2339
  const ctx = useDatePickerContext("MonthPicker.Grid");
2203
- const { adapter, viewMonth, locale, value, displayTimezone, labels } = ctx;
2340
+ const { adapter, viewMonth, locale, value, displayTimezone, labels, disabled } = ctx;
2204
2341
  const currentYear = adapter.getYear(viewMonth);
2205
2342
  const [valueYear, valueMonthZeroBased] = useMemo(() => {
2206
2343
  if (!value) return [null, null];
@@ -2211,9 +2348,19 @@ function MonthPickerGrid({ classNames, ...props }) {
2211
2348
  return [null, null];
2212
2349
  }
2213
2350
  }, [value, adapter, displayTimezone]);
2214
- const today = adapter.today(displayTimezone);
2215
- const todayYear = adapter.getYear(today);
2216
- const todayMonth = adapter.getMonth(today);
2351
+ const [today, setToday] = useState(null);
2352
+ useEffect(() => {
2353
+ setToday(adapter.today(displayTimezone));
2354
+ }, [adapter, displayTimezone]);
2355
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
2356
+ const todayMonth = today !== null ? adapter.getMonth(today) : -1;
2357
+ const monthDisabledFlags = useMemo(
2358
+ () => Array.from({ length: 12 }, (_, i) => {
2359
+ const monthStart = new Date(Date.UTC(currentYear, i, 1)).toISOString();
2360
+ return isRangeFullyDisabled(monthStart, adapter.endOfMonth(monthStart), disabled, adapter);
2361
+ }),
2362
+ [currentYear, disabled, adapter]
2363
+ );
2217
2364
  const navigateYear = useCallback(
2218
2365
  (direction) => {
2219
2366
  ctx.setViewMonth(adapter.addYears(viewMonth, direction));
@@ -2222,17 +2369,23 @@ function MonthPickerGrid({ classNames, ...props }) {
2222
2369
  );
2223
2370
  const handleMonthSelect = useCallback(
2224
2371
  (monthIndex) => {
2372
+ if (monthDisabledFlags[monthIndex]) return;
2225
2373
  const target = new Date(Date.UTC(currentYear, monthIndex, 1)).toISOString();
2226
2374
  ctx.selectDate(target);
2227
2375
  },
2228
- [currentYear, ctx]
2229
- );
2230
- const months = Array.from({ length: 12 }, (_, i) => ({
2231
- index: i,
2232
- name: getMonthName(i, locale),
2233
- isSelected: valueYear === currentYear && valueMonthZeroBased === i,
2234
- isCurrent: todayYear === currentYear && todayMonth === i
2235
- }));
2376
+ [currentYear, ctx, monthDisabledFlags]
2377
+ );
2378
+ const naturalIndex = valueYear === currentYear && valueMonthZeroBased !== null ? valueMonthZeroBased : adapter.getMonth(viewMonth);
2379
+ const firstEnabled = monthDisabledFlags.findIndex((d) => !d);
2380
+ const initialIndex = monthDisabledFlags[naturalIndex] ? firstEnabled === -1 ? naturalIndex : firstEnabled : naturalIndex;
2381
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
2382
+ initialIndex,
2383
+ disabledFlags: monthDisabledFlags,
2384
+ onSelect: handleMonthSelect,
2385
+ onPageUp: () => navigateYear(-1),
2386
+ onPageDown: () => navigateYear(1),
2387
+ onEscape: ctx.close
2388
+ });
2236
2389
  return /* @__PURE__ */ jsxs("div", { className: classNames?.root, ...props, children: [
2237
2390
  /* @__PURE__ */ jsxs("div", { className: classNames?.header, children: [
2238
2391
  /* @__PURE__ */ jsx(
@@ -2257,37 +2410,57 @@ function MonthPickerGrid({ classNames, ...props }) {
2257
2410
  }
2258
2411
  )
2259
2412
  ] }),
2260
- /* @__PURE__ */ jsx("div", { role: "grid", "aria-label": `${currentYear} months`, className: classNames?.grid, children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsx(
2413
+ /* @__PURE__ */ jsx(
2261
2414
  "div",
2262
2415
  {
2263
- role: "row",
2264
- className: classNames?.gridRow,
2265
- style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2266
- children: months.slice(rowIndex * 3, rowIndex * 3 + 3).map((m) => {
2267
- const monthClass = [
2268
- classNames?.month,
2269
- m.isSelected && classNames?.monthSelected,
2270
- m.isCurrent && classNames?.monthCurrent
2271
- ].filter(Boolean).join(" ") || void 0;
2272
- return /* @__PURE__ */ jsx(
2273
- "button",
2274
- {
2275
- type: "button",
2276
- role: "gridcell",
2277
- "aria-selected": m.isSelected || void 0,
2278
- "aria-current": m.isCurrent ? "date" : void 0,
2279
- "data-selected": m.isSelected || void 0,
2280
- "data-current": m.isCurrent || void 0,
2281
- className: monthClass,
2282
- onClick: () => handleMonthSelect(m.index),
2283
- children: m.name
2284
- },
2285
- m.index
2286
- );
2287
- })
2288
- },
2289
- rowIndex
2290
- )) })
2416
+ ref: gridRef,
2417
+ role: "grid",
2418
+ "aria-label": `${currentYear} months`,
2419
+ className: classNames?.grid,
2420
+ onKeyDown: handleKeyDown,
2421
+ children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsx(
2422
+ "div",
2423
+ {
2424
+ role: "row",
2425
+ className: classNames?.gridRow,
2426
+ style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2427
+ children: Array.from({ length: 3 }, (_2, col) => {
2428
+ const i = rowIndex * 3 + col;
2429
+ const isSelected = valueYear === currentYear && valueMonthZeroBased === i;
2430
+ const isCurrent = todayYear === currentYear && todayMonth === i;
2431
+ const isFocused = i === focusedIndex;
2432
+ const isDisabled = monthDisabledFlags[i] ?? false;
2433
+ const cls = [
2434
+ classNames?.month,
2435
+ isSelected && classNames?.monthSelected,
2436
+ isCurrent && classNames?.monthCurrent,
2437
+ isDisabled && classNames?.monthDisabled
2438
+ ].filter(Boolean).join(" ") || void 0;
2439
+ return /* @__PURE__ */ jsx(
2440
+ "button",
2441
+ {
2442
+ type: "button",
2443
+ role: "gridcell",
2444
+ tabIndex: isFocused ? 0 : -1,
2445
+ disabled: isDisabled,
2446
+ "aria-selected": isSelected || void 0,
2447
+ "aria-disabled": isDisabled || void 0,
2448
+ "aria-current": isCurrent ? "date" : void 0,
2449
+ "data-selected": isSelected || void 0,
2450
+ "data-current": isCurrent || void 0,
2451
+ "data-focused": isFocused || void 0,
2452
+ className: cls,
2453
+ onClick: () => handleMonthSelect(i),
2454
+ children: getMonthName(i, locale)
2455
+ },
2456
+ i
2457
+ );
2458
+ })
2459
+ },
2460
+ rowIndex
2461
+ ))
2462
+ }
2463
+ )
2291
2464
  ] });
2292
2465
  }
2293
2466
 
@@ -2304,7 +2477,7 @@ function YearPickerRoot(props) {
2304
2477
  }
2305
2478
  function YearPickerGrid({ classNames, ...props }) {
2306
2479
  const ctx = useDatePickerContext("YearPicker.Grid");
2307
- const { adapter, viewMonth, value, displayTimezone, labels } = ctx;
2480
+ const { adapter, viewMonth, value, displayTimezone, labels, disabled } = ctx;
2308
2481
  const currentYear = adapter.getYear(viewMonth);
2309
2482
  const decadeStart = currentYear - currentYear % 12;
2310
2483
  const valueYear = useMemo(() => {
@@ -2315,7 +2488,20 @@ function YearPickerGrid({ classNames, ...props }) {
2315
2488
  return null;
2316
2489
  }
2317
2490
  }, [value, adapter, displayTimezone]);
2318
- const todayYear = adapter.getYear(adapter.today(displayTimezone));
2491
+ const [today, setToday] = useState(null);
2492
+ useEffect(() => {
2493
+ setToday(adapter.today(displayTimezone));
2494
+ }, [adapter, displayTimezone]);
2495
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
2496
+ const yearDisabledFlags = useMemo(
2497
+ () => Array.from({ length: 12 }, (_, i) => {
2498
+ const year = decadeStart + i;
2499
+ const yearStart = new Date(Date.UTC(year, 0, 1)).toISOString();
2500
+ const yearEnd = new Date(Date.UTC(year, 11, 31, 23, 59, 59, 999)).toISOString();
2501
+ return isRangeFullyDisabled(yearStart, yearEnd, disabled, adapter);
2502
+ }),
2503
+ [decadeStart, disabled, adapter]
2504
+ );
2319
2505
  const navigateDecade = useCallback(
2320
2506
  (direction) => {
2321
2507
  ctx.setViewMonth(adapter.addYears(viewMonth, direction * 12));
@@ -2323,19 +2509,24 @@ function YearPickerGrid({ classNames, ...props }) {
2323
2509
  [adapter, viewMonth, ctx]
2324
2510
  );
2325
2511
  const handleYearSelect = useCallback(
2326
- (year) => {
2512
+ (indexInDecade) => {
2513
+ if (yearDisabledFlags[indexInDecade]) return;
2514
+ const year = decadeStart + indexInDecade;
2327
2515
  const target = new Date(Date.UTC(year, 0, 1)).toISOString();
2328
2516
  ctx.selectDate(target);
2329
2517
  },
2330
- [ctx]
2331
- );
2332
- const years = Array.from({ length: 12 }, (_, i) => {
2333
- const year = decadeStart + i;
2334
- return {
2335
- value: year,
2336
- isSelected: year === valueYear,
2337
- isCurrent: year === todayYear
2338
- };
2518
+ [ctx, decadeStart, yearDisabledFlags]
2519
+ );
2520
+ const naturalIndex = valueYear !== null && valueYear >= decadeStart && valueYear <= decadeStart + 11 ? valueYear - decadeStart : currentYear - decadeStart;
2521
+ const firstEnabled = yearDisabledFlags.findIndex((d) => !d);
2522
+ const initialIndex = yearDisabledFlags[naturalIndex] ? firstEnabled === -1 ? naturalIndex : firstEnabled : naturalIndex;
2523
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
2524
+ initialIndex,
2525
+ disabledFlags: yearDisabledFlags,
2526
+ onSelect: handleYearSelect,
2527
+ onPageUp: () => navigateDecade(-1),
2528
+ onPageDown: () => navigateDecade(1),
2529
+ onEscape: ctx.close
2339
2530
  });
2340
2531
  const rangeLabel = `${decadeStart}\u2013${decadeStart + 11}`;
2341
2532
  return /* @__PURE__ */ jsxs("div", { className: classNames?.root, ...props, children: [
@@ -2362,37 +2553,58 @@ function YearPickerGrid({ classNames, ...props }) {
2362
2553
  }
2363
2554
  )
2364
2555
  ] }),
2365
- /* @__PURE__ */ jsx("div", { role: "grid", "aria-label": rangeLabel, className: classNames?.grid, children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsx(
2556
+ /* @__PURE__ */ jsx(
2366
2557
  "div",
2367
2558
  {
2368
- role: "row",
2369
- className: classNames?.gridRow,
2370
- style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2371
- children: years.slice(rowIndex * 3, rowIndex * 3 + 3).map((y) => {
2372
- const yearClass = [
2373
- classNames?.year,
2374
- y.isSelected && classNames?.yearSelected,
2375
- y.isCurrent && classNames?.yearCurrent
2376
- ].filter(Boolean).join(" ") || void 0;
2377
- return /* @__PURE__ */ jsx(
2378
- "button",
2379
- {
2380
- type: "button",
2381
- role: "gridcell",
2382
- "aria-selected": y.isSelected || void 0,
2383
- "aria-current": y.isCurrent ? "date" : void 0,
2384
- "data-selected": y.isSelected || void 0,
2385
- "data-current": y.isCurrent || void 0,
2386
- className: yearClass,
2387
- onClick: () => handleYearSelect(y.value),
2388
- children: y.value
2389
- },
2390
- y.value
2391
- );
2392
- })
2393
- },
2394
- rowIndex
2395
- )) })
2559
+ ref: gridRef,
2560
+ role: "grid",
2561
+ "aria-label": rangeLabel,
2562
+ className: classNames?.grid,
2563
+ onKeyDown: handleKeyDown,
2564
+ children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsx(
2565
+ "div",
2566
+ {
2567
+ role: "row",
2568
+ className: classNames?.gridRow,
2569
+ style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2570
+ children: Array.from({ length: 3 }, (_2, col) => {
2571
+ const i = rowIndex * 3 + col;
2572
+ const year = decadeStart + i;
2573
+ const isSelected = year === valueYear;
2574
+ const isCurrent = year === todayYear;
2575
+ const isFocused = i === focusedIndex;
2576
+ const isDisabled = yearDisabledFlags[i] ?? false;
2577
+ const cls = [
2578
+ classNames?.year,
2579
+ isSelected && classNames?.yearSelected,
2580
+ isCurrent && classNames?.yearCurrent,
2581
+ isDisabled && classNames?.yearDisabled
2582
+ ].filter(Boolean).join(" ") || void 0;
2583
+ return /* @__PURE__ */ jsx(
2584
+ "button",
2585
+ {
2586
+ type: "button",
2587
+ role: "gridcell",
2588
+ tabIndex: isFocused ? 0 : -1,
2589
+ disabled: isDisabled,
2590
+ "aria-selected": isSelected || void 0,
2591
+ "aria-disabled": isDisabled || void 0,
2592
+ "aria-current": isCurrent ? "date" : void 0,
2593
+ "data-selected": isSelected || void 0,
2594
+ "data-current": isCurrent || void 0,
2595
+ "data-focused": isFocused || void 0,
2596
+ className: cls,
2597
+ onClick: () => handleYearSelect(i),
2598
+ children: year
2599
+ },
2600
+ i
2601
+ );
2602
+ })
2603
+ },
2604
+ rowIndex
2605
+ ))
2606
+ }
2607
+ )
2396
2608
  ] });
2397
2609
  }
2398
2610