@kalyx/react 1.0.0-rc.4 → 1.0.0-rc.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,88 @@
1
1
  # @kalyx/react
2
2
 
3
+ ## 1.0.0-rc.6
4
+
5
+ ### Patch Changes
6
+
7
+ - abc56ac: Security: pin transitive `fast-uri` to `>=3.1.2` and `@babel/plugin-transform-modules-systemjs` to `>=7.29.4` via `pnpm.overrides`.
8
+
9
+ Resolves three Code Scanning alerts on `pnpm-lock.yaml`:
10
+ - `fast-uri@3.1.0` — [GHSA-v39h-62p7-jpjc](https://osv.dev/GHSA-v39h-62p7-jpjc) (CVE-2026-6322), first patched in `3.1.2`.
11
+ - `fast-uri@3.1.0` — [GHSA-q3j6-qgpj-74h6](https://osv.dev/GHSA-q3j6-qgpj-74h6) (CVE-2026-6321), first patched in `3.1.1`.
12
+ - `@babel/plugin-transform-modules-systemjs@7.29.0` — [GHSA-fv7c-fp4j-7gwp](https://osv.dev/GHSA-fv7c-fp4j-7gwp) (CVE-2026-44728), first patched in `7.29.4` on the 7.x line.
13
+
14
+ All three packages are transitive build-time dependencies (ajv → fast-uri, Babel preset-env → systemjs plugin); no public API impact.
15
+
16
+ - Updated dependencies [abc56ac]
17
+ - @kalyx/core@1.0.0-rc.6
18
+
19
+ ## 1.0.0-rc.5
20
+
21
+ ### Patch Changes
22
+
23
+ - 9f3cf9b: WAI-ARIA grid keyboard navigation for the four 3×4 picker grids
24
+ (`DatePicker.MonthGrid`, `DatePicker.YearGrid`, `MonthPicker.Grid`,
25
+ `YearPicker.Grid`).
26
+
27
+ Before, these grids declared `role="grid"` but had no key handler — keyboard
28
+ users could not select a month or year, in violation of CLAUDE.md §7.
29
+
30
+ Now each grid implements:
31
+ - **Arrow keys** — ±1 column / ±3 rows, clamped to grid bounds.
32
+ - **Home / End** — first / last cell of the current row.
33
+ - **PageUp / PageDown** — previous / next year (or decade for year grids).
34
+ - **Enter / Space** — commit the focused cell (drilldown grids switch view via
35
+ `onSelect`; commit grids close the popover via `ctx.selectDate`).
36
+ - **Roving tabIndex** — only the focused cell has `tabIndex=0`; the
37
+ `data-focused` attribute follows.
38
+ - **Auto-refocus** — DOM focus moves with `focusedIndex` so PageUp/Down lands
39
+ the user back on the same column position. Cells use stable index keys so
40
+ the buttons persist across page nav.
41
+
42
+ Component-level integration tests added per CLAUDE.md §7 across `DatePicker`,
43
+ `RangePicker`, `DateTimePicker`, and `WeekPicker`: leap-year (Feb 29 2024)
44
+ click commit, `before`/`after` rule click block, `dayOfWeek` rule click block
45
+ plus visual `aria-disabled`, and keyboard ArrowLeft skip-disabled.
46
+
47
+ **Bundle target raised to 14 KB** — full grid keyboard nav (state + handlers
48
+ - auto-refocus) added ~1.4 KB gzip across the four grids. Measured 12.85 KB
49
+ ESM / 13.64 KB CJS at this point. README, docs, `scripts/check-bundle-size.js`,
50
+ PR template, and CI gate updated to ≤14 KB.
51
+
52
+ **Internal:** new shared `useGridState` hook in
53
+ `packages/react/src/components/_shared/grid-keyboard.ts` (not exported from
54
+ the package public API) consolidates keyboard handling and roving-focus
55
+ state across all four grids.
56
+
57
+ - 9b19df4: `MonthPicker.Grid` and `YearPicker.Grid` now respect `before` / `after`
58
+ disabled rules — months/years that fall entirely outside the allowed range
59
+ are rendered with the `disabled` HTML attribute, `aria-disabled="true"`, the
60
+ new `monthDisabled` / `yearDisabled` className slots, and are skipped during
61
+ keyboard navigation.
62
+
63
+ This was deliberately deferred from PR #46 to keep that bundle under 14 KB;
64
+ it lands now with a 14 → 15 KB ceiling bump.
65
+
66
+ Behavioral details:
67
+ - A month is "fully disabled" only when every day in it is excluded by a
68
+ `before` or `after` rule. `date` and `dayOfWeek` rules can never disable a
69
+ whole month, so they remain a per-day concern.
70
+ - A year follows the same rule against `[Jan 1 00:00:00, Dec 31 23:59:59.999]`.
71
+ - Click and keyboard `Enter` / `Space` on a disabled cell are no-ops.
72
+ - Initial focus and post-PageUp/PageDown focus both re-anchor to the first
73
+ enabled cell when the natural target is itself disabled. (A `disabled`
74
+ HTML button can't receive DOM focus, so without the re-anchor the user
75
+ would silently lose keyboard navigation.)
76
+
77
+ **Internal:** `useGridState` regains its optional `disabledFlags` parameter
78
+ plus a focus re-anchor effect; `isRangeFullyDisabled` is reintroduced as an
79
+ internal helper. Neither is exposed in the package public API.
80
+
81
+ **Bundle target:** raised 14 → 15 KB (measured 13.96 KB ESM / 14.21 KB CJS).
82
+ Same precedent as the 12 → 13 KB and 13 → 14 KB bumps when prior feature
83
+ work landed. Updated `scripts/check-bundle-size.js`, `pr-check.yml`, READMEs,
84
+ CLAUDE.md, PR template, and `check-bundle.md`.
85
+
3
86
  ## 1.0.0-rc.4
4
87
 
5
88
  ### Patch Changes
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @kalyx/react
2
2
 
3
- > The headless React DatePicker, finally complete. Zero CSS · SSR-safe · ~12 KB gzip (≤ 13 KB ceiling).
3
+ > The headless React DatePicker, finally complete. Zero CSS · SSR-safe · ~14 KB gzip (≤ 15 KB ceiling).
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@kalyx/react?color=5b4fe1)](https://www.npmjs.com/package/@kalyx/react)
6
- [![Bundle](https://img.shields.io/badge/gzip-12.27KB-brightgreen)](https://kalyx-docs.vercel.app/docs/api/react#bundle-size)
6
+ [![Bundle](https://img.shields.io/badge/gzip-13.60KB-brightgreen)](https://kalyx-docs.vercel.app/docs/api/react#bundle-size)
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](https://www.typescriptlang.org/)
8
8
  [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/jiji-hoon96/kalyx/blob/main/LICENSE)
9
9
 
package/dist/index.cjs CHANGED
@@ -686,6 +686,86 @@ function DatePickerCalendar({
686
686
  /* @__PURE__ */ jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", style: srOnly, children: announcement })
687
687
  ] });
688
688
  }
689
+ function isRangeFullyDisabled(start, end, rules, adapter) {
690
+ for (const rule of rules) {
691
+ if ("before" in rule && adapter.isBefore(end, rule.before)) return true;
692
+ if ("after" in rule && adapter.isAfter(start, rule.after)) return true;
693
+ }
694
+ return false;
695
+ }
696
+ function useGridState(opts) {
697
+ const { initialIndex, disabledFlags, onSelect, onPageUp, onPageDown, onEscape } = opts;
698
+ const gridRef = react.useRef(null);
699
+ const [focusedIndex, setFocusedIndex] = react.useState(initialIndex);
700
+ const handleKeyDown = (e) => {
701
+ let next = null;
702
+ let step = 1;
703
+ switch (e.key) {
704
+ case "ArrowLeft":
705
+ next = Math.max(0, focusedIndex - 1);
706
+ step = -1;
707
+ break;
708
+ case "ArrowRight":
709
+ next = Math.min(11, focusedIndex + 1);
710
+ break;
711
+ case "ArrowUp":
712
+ next = Math.max(0, focusedIndex - 3);
713
+ step = -1;
714
+ break;
715
+ case "ArrowDown":
716
+ next = Math.min(11, focusedIndex + 3);
717
+ break;
718
+ case "Home":
719
+ next = focusedIndex - focusedIndex % 3;
720
+ step = -1;
721
+ break;
722
+ case "End":
723
+ next = focusedIndex - focusedIndex % 3 + 2;
724
+ break;
725
+ case "PageUp":
726
+ e.preventDefault();
727
+ onPageUp();
728
+ return;
729
+ case "PageDown":
730
+ e.preventDefault();
731
+ onPageDown();
732
+ return;
733
+ case "Enter":
734
+ case " ":
735
+ e.preventDefault();
736
+ onSelect(focusedIndex);
737
+ return;
738
+ case "Escape":
739
+ onEscape();
740
+ return;
741
+ default:
742
+ return;
743
+ }
744
+ if (next === null) return;
745
+ e.preventDefault();
746
+ if (disabledFlags) {
747
+ let attempts = 0;
748
+ while (next >= 0 && next < 12 && disabledFlags[next] && attempts < 12) {
749
+ next += step;
750
+ attempts++;
751
+ }
752
+ if (next < 0 || next >= 12 || disabledFlags[next]) return;
753
+ }
754
+ if (next !== focusedIndex) setFocusedIndex(next);
755
+ };
756
+ react.useEffect(() => {
757
+ if (!disabledFlags || !disabledFlags[focusedIndex]) return;
758
+ const firstEnabled = disabledFlags.findIndex((d) => !d);
759
+ if (firstEnabled !== -1 && firstEnabled !== focusedIndex) {
760
+ setFocusedIndex(firstEnabled);
761
+ }
762
+ }, [disabledFlags, focusedIndex]);
763
+ react.useEffect(() => {
764
+ const btn = gridRef.current?.querySelector('[data-focused="true"]');
765
+ btn?.focus({ preventScroll: true });
766
+ }, [focusedIndex]);
767
+ return { gridRef, focusedIndex, handleKeyDown };
768
+ }
689
769
  function DatePickerMonthGrid({
690
770
  classNames,
691
771
  onSelect,
@@ -704,8 +784,7 @@ function DatePickerMonthGrid({
704
784
  const todayYear = today !== null ? adapter.getYear(today) : -1;
705
785
  const navigateYear = react.useCallback(
706
786
  (direction) => {
707
- const newDate = adapter.addYears(viewMonth, direction);
708
- ctx.setViewMonth(newDate);
787
+ ctx.setViewMonth(adapter.addYears(viewMonth, direction));
709
788
  },
710
789
  [adapter, viewMonth, ctx]
711
790
  );
@@ -718,12 +797,13 @@ function DatePickerMonthGrid({
718
797
  },
719
798
  [currentYear, ctx, onSelect]
720
799
  );
721
- const months = Array.from({ length: 12 }, (_, i) => ({
722
- index: i,
723
- name: core.getMonthName(i, locale),
724
- isSelected: i === currentMonth,
725
- isCurrent: i === todayMonth && currentYear === todayYear
726
- }));
800
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
801
+ initialIndex: currentMonth,
802
+ onSelect: handleMonthSelect,
803
+ onPageUp: () => navigateYear(-1),
804
+ onPageDown: () => navigateYear(1),
805
+ onEscape: ctx.close
806
+ });
727
807
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.root, ...props, children: [
728
808
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.header, children: [
729
809
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -751,30 +831,37 @@ function DatePickerMonthGrid({
751
831
  /* @__PURE__ */ jsxRuntime.jsx(
752
832
  "div",
753
833
  {
834
+ ref: gridRef,
754
835
  role: "grid",
755
836
  "aria-label": `${currentYear} months`,
756
837
  className: classNames?.grid,
757
838
  style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
758
- children: months.map((m) => {
759
- const monthClass = [
839
+ onKeyDown: handleKeyDown,
840
+ children: Array.from({ length: 12 }, (_, i) => {
841
+ const isSelected = i === currentMonth;
842
+ const isCurrent = i === todayMonth && currentYear === todayYear;
843
+ const isFocused = i === focusedIndex;
844
+ const cls = [
760
845
  classNames?.month,
761
- m.isSelected && classNames?.monthSelected,
762
- m.isCurrent && classNames?.monthCurrent
846
+ isSelected && classNames?.monthSelected,
847
+ isCurrent && classNames?.monthCurrent
763
848
  ].filter(Boolean).join(" ") || void 0;
764
849
  return /* @__PURE__ */ jsxRuntime.jsx(
765
850
  "button",
766
851
  {
767
852
  type: "button",
768
853
  role: "gridcell",
769
- "aria-selected": m.isSelected || void 0,
770
- "aria-current": m.isCurrent ? "date" : void 0,
771
- "data-selected": m.isSelected || void 0,
772
- "data-current": m.isCurrent || void 0,
773
- className: monthClass,
774
- onClick: () => handleMonthSelect(m.index),
775
- children: m.name
854
+ tabIndex: isFocused ? 0 : -1,
855
+ "aria-selected": isSelected || void 0,
856
+ "aria-current": isCurrent ? "date" : void 0,
857
+ "data-selected": isSelected || void 0,
858
+ "data-current": isCurrent || void 0,
859
+ "data-focused": isFocused || void 0,
860
+ className: cls,
861
+ onClick: () => handleMonthSelect(i),
862
+ children: core.getMonthName(i, locale)
776
863
  },
777
- m.index
864
+ i
778
865
  );
779
866
  })
780
867
  }
@@ -793,32 +880,28 @@ function DatePickerYearGrid({ classNames, onSelect, ...props }) {
793
880
  const decadeStart = currentYear - currentYear % 12;
794
881
  const navigateDecade = react.useCallback(
795
882
  (direction) => {
796
- const newDate = adapter.addYears(viewMonth, direction * 12);
797
- ctx.setViewMonth(newDate);
883
+ ctx.setViewMonth(adapter.addYears(viewMonth, direction * 12));
798
884
  },
799
885
  [adapter, viewMonth, ctx]
800
886
  );
801
887
  const handleYearSelect = react.useCallback(
802
- (year) => {
888
+ (indexInDecade) => {
889
+ const year = decadeStart + indexInDecade;
803
890
  const currentMonth = adapter.getMonth(viewMonth);
804
891
  const target = new Date(Date.UTC(year, currentMonth, 1)).toISOString();
805
892
  ctx.setViewMonth(target);
806
893
  ctx.setFocusedDate(target);
807
894
  onSelect?.();
808
895
  },
809
- [adapter, viewMonth, ctx, onSelect]
810
- );
811
- const years = react.useMemo(
812
- () => Array.from({ length: 12 }, (_, i) => {
813
- const year = decadeStart + i;
814
- return {
815
- value: year,
816
- isSelected: year === currentYear,
817
- isCurrent: year === todayYear
818
- };
819
- }),
820
- [decadeStart, currentYear, todayYear]
821
- );
896
+ [adapter, viewMonth, ctx, onSelect, decadeStart]
897
+ );
898
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
899
+ initialIndex: currentYear - decadeStart,
900
+ onSelect: handleYearSelect,
901
+ onPageUp: () => navigateDecade(-1),
902
+ onPageDown: () => navigateDecade(1),
903
+ onEscape: ctx.close
904
+ });
822
905
  const rangeLabel = `${decadeStart}\u2013${decadeStart + 11}`;
823
906
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.root, ...props, children: [
824
907
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.header, children: [
@@ -847,30 +930,38 @@ function DatePickerYearGrid({ classNames, onSelect, ...props }) {
847
930
  /* @__PURE__ */ jsxRuntime.jsx(
848
931
  "div",
849
932
  {
933
+ ref: gridRef,
850
934
  role: "grid",
851
935
  "aria-label": rangeLabel,
852
936
  className: classNames?.grid,
853
937
  style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
854
- children: years.map((y) => {
855
- const yearClass = [
938
+ onKeyDown: handleKeyDown,
939
+ children: Array.from({ length: 12 }, (_, i) => {
940
+ const year = decadeStart + i;
941
+ const isSelected = year === currentYear;
942
+ const isCurrent = year === todayYear;
943
+ const isFocused = i === focusedIndex;
944
+ const cls = [
856
945
  classNames?.year,
857
- y.isSelected && classNames?.yearSelected,
858
- y.isCurrent && classNames?.yearCurrent
946
+ isSelected && classNames?.yearSelected,
947
+ isCurrent && classNames?.yearCurrent
859
948
  ].filter(Boolean).join(" ") || void 0;
860
949
  return /* @__PURE__ */ jsxRuntime.jsx(
861
950
  "button",
862
951
  {
863
952
  type: "button",
864
953
  role: "gridcell",
865
- "aria-selected": y.isSelected || void 0,
866
- "aria-current": y.isCurrent ? "date" : void 0,
867
- "data-selected": y.isSelected || void 0,
868
- "data-current": y.isCurrent || void 0,
869
- className: yearClass,
870
- onClick: () => handleYearSelect(y.value),
871
- children: y.value
954
+ tabIndex: isFocused ? 0 : -1,
955
+ "aria-selected": isSelected || void 0,
956
+ "aria-current": isCurrent ? "date" : void 0,
957
+ "data-selected": isSelected || void 0,
958
+ "data-current": isCurrent || void 0,
959
+ "data-focused": isFocused || void 0,
960
+ className: cls,
961
+ onClick: () => handleYearSelect(i),
962
+ children: year
872
963
  },
873
- y.value
964
+ i
874
965
  );
875
966
  })
876
967
  }
@@ -2247,7 +2338,7 @@ function MonthPickerRoot(props) {
2247
2338
  }
2248
2339
  function MonthPickerGrid({ classNames, ...props }) {
2249
2340
  const ctx = useDatePickerContext("MonthPicker.Grid");
2250
- const { adapter, viewMonth, locale, value, displayTimezone, labels } = ctx;
2341
+ const { adapter, viewMonth, locale, value, displayTimezone, labels, disabled } = ctx;
2251
2342
  const currentYear = adapter.getYear(viewMonth);
2252
2343
  const [valueYear, valueMonthZeroBased] = react.useMemo(() => {
2253
2344
  if (!value) return [null, null];
@@ -2264,6 +2355,13 @@ function MonthPickerGrid({ classNames, ...props }) {
2264
2355
  }, [adapter, displayTimezone]);
2265
2356
  const todayYear = today !== null ? adapter.getYear(today) : -1;
2266
2357
  const todayMonth = today !== null ? adapter.getMonth(today) : -1;
2358
+ const monthDisabledFlags = react.useMemo(
2359
+ () => Array.from({ length: 12 }, (_, i) => {
2360
+ const monthStart = new Date(Date.UTC(currentYear, i, 1)).toISOString();
2361
+ return isRangeFullyDisabled(monthStart, adapter.endOfMonth(monthStart), disabled, adapter);
2362
+ }),
2363
+ [currentYear, disabled, adapter]
2364
+ );
2267
2365
  const navigateYear = react.useCallback(
2268
2366
  (direction) => {
2269
2367
  ctx.setViewMonth(adapter.addYears(viewMonth, direction));
@@ -2272,17 +2370,23 @@ function MonthPickerGrid({ classNames, ...props }) {
2272
2370
  );
2273
2371
  const handleMonthSelect = react.useCallback(
2274
2372
  (monthIndex) => {
2373
+ if (monthDisabledFlags[monthIndex]) return;
2275
2374
  const target = new Date(Date.UTC(currentYear, monthIndex, 1)).toISOString();
2276
2375
  ctx.selectDate(target);
2277
2376
  },
2278
- [currentYear, ctx]
2279
- );
2280
- const months = Array.from({ length: 12 }, (_, i) => ({
2281
- index: i,
2282
- name: core.getMonthName(i, locale),
2283
- isSelected: valueYear === currentYear && valueMonthZeroBased === i,
2284
- isCurrent: todayYear === currentYear && todayMonth === i
2285
- }));
2377
+ [currentYear, ctx, monthDisabledFlags]
2378
+ );
2379
+ const naturalIndex = valueYear === currentYear && valueMonthZeroBased !== null ? valueMonthZeroBased : adapter.getMonth(viewMonth);
2380
+ const firstEnabled = monthDisabledFlags.findIndex((d) => !d);
2381
+ const initialIndex = monthDisabledFlags[naturalIndex] ? firstEnabled === -1 ? naturalIndex : firstEnabled : naturalIndex;
2382
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
2383
+ initialIndex,
2384
+ disabledFlags: monthDisabledFlags,
2385
+ onSelect: handleMonthSelect,
2386
+ onPageUp: () => navigateYear(-1),
2387
+ onPageDown: () => navigateYear(1),
2388
+ onEscape: ctx.close
2389
+ });
2286
2390
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.root, ...props, children: [
2287
2391
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.header, children: [
2288
2392
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2307,37 +2411,57 @@ function MonthPickerGrid({ classNames, ...props }) {
2307
2411
  }
2308
2412
  )
2309
2413
  ] }),
2310
- /* @__PURE__ */ jsxRuntime.jsx("div", { role: "grid", "aria-label": `${currentYear} months`, className: classNames?.grid, children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsxRuntime.jsx(
2414
+ /* @__PURE__ */ jsxRuntime.jsx(
2311
2415
  "div",
2312
2416
  {
2313
- role: "row",
2314
- className: classNames?.gridRow,
2315
- style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2316
- children: months.slice(rowIndex * 3, rowIndex * 3 + 3).map((m) => {
2317
- const monthClass = [
2318
- classNames?.month,
2319
- m.isSelected && classNames?.monthSelected,
2320
- m.isCurrent && classNames?.monthCurrent
2321
- ].filter(Boolean).join(" ") || void 0;
2322
- return /* @__PURE__ */ jsxRuntime.jsx(
2323
- "button",
2324
- {
2325
- type: "button",
2326
- role: "gridcell",
2327
- "aria-selected": m.isSelected || void 0,
2328
- "aria-current": m.isCurrent ? "date" : void 0,
2329
- "data-selected": m.isSelected || void 0,
2330
- "data-current": m.isCurrent || void 0,
2331
- className: monthClass,
2332
- onClick: () => handleMonthSelect(m.index),
2333
- children: m.name
2334
- },
2335
- m.index
2336
- );
2337
- })
2338
- },
2339
- rowIndex
2340
- )) })
2417
+ ref: gridRef,
2418
+ role: "grid",
2419
+ "aria-label": `${currentYear} months`,
2420
+ className: classNames?.grid,
2421
+ onKeyDown: handleKeyDown,
2422
+ children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsxRuntime.jsx(
2423
+ "div",
2424
+ {
2425
+ role: "row",
2426
+ className: classNames?.gridRow,
2427
+ style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2428
+ children: Array.from({ length: 3 }, (_2, col) => {
2429
+ const i = rowIndex * 3 + col;
2430
+ const isSelected = valueYear === currentYear && valueMonthZeroBased === i;
2431
+ const isCurrent = todayYear === currentYear && todayMonth === i;
2432
+ const isFocused = i === focusedIndex;
2433
+ const isDisabled = monthDisabledFlags[i] ?? false;
2434
+ const cls = [
2435
+ classNames?.month,
2436
+ isSelected && classNames?.monthSelected,
2437
+ isCurrent && classNames?.monthCurrent,
2438
+ isDisabled && classNames?.monthDisabled
2439
+ ].filter(Boolean).join(" ") || void 0;
2440
+ return /* @__PURE__ */ jsxRuntime.jsx(
2441
+ "button",
2442
+ {
2443
+ type: "button",
2444
+ role: "gridcell",
2445
+ tabIndex: isFocused ? 0 : -1,
2446
+ disabled: isDisabled,
2447
+ "aria-selected": isSelected || void 0,
2448
+ "aria-disabled": isDisabled || void 0,
2449
+ "aria-current": isCurrent ? "date" : void 0,
2450
+ "data-selected": isSelected || void 0,
2451
+ "data-current": isCurrent || void 0,
2452
+ "data-focused": isFocused || void 0,
2453
+ className: cls,
2454
+ onClick: () => handleMonthSelect(i),
2455
+ children: core.getMonthName(i, locale)
2456
+ },
2457
+ i
2458
+ );
2459
+ })
2460
+ },
2461
+ rowIndex
2462
+ ))
2463
+ }
2464
+ )
2341
2465
  ] });
2342
2466
  }
2343
2467
 
@@ -2354,7 +2478,7 @@ function YearPickerRoot(props) {
2354
2478
  }
2355
2479
  function YearPickerGrid({ classNames, ...props }) {
2356
2480
  const ctx = useDatePickerContext("YearPicker.Grid");
2357
- const { adapter, viewMonth, value, displayTimezone, labels } = ctx;
2481
+ const { adapter, viewMonth, value, displayTimezone, labels, disabled } = ctx;
2358
2482
  const currentYear = adapter.getYear(viewMonth);
2359
2483
  const decadeStart = currentYear - currentYear % 12;
2360
2484
  const valueYear = react.useMemo(() => {
@@ -2370,6 +2494,15 @@ function YearPickerGrid({ classNames, ...props }) {
2370
2494
  setToday(adapter.today(displayTimezone));
2371
2495
  }, [adapter, displayTimezone]);
2372
2496
  const todayYear = today !== null ? adapter.getYear(today) : -1;
2497
+ const yearDisabledFlags = react.useMemo(
2498
+ () => Array.from({ length: 12 }, (_, i) => {
2499
+ const year = decadeStart + i;
2500
+ const yearStart = new Date(Date.UTC(year, 0, 1)).toISOString();
2501
+ const yearEnd = new Date(Date.UTC(year, 11, 31, 23, 59, 59, 999)).toISOString();
2502
+ return isRangeFullyDisabled(yearStart, yearEnd, disabled, adapter);
2503
+ }),
2504
+ [decadeStart, disabled, adapter]
2505
+ );
2373
2506
  const navigateDecade = react.useCallback(
2374
2507
  (direction) => {
2375
2508
  ctx.setViewMonth(adapter.addYears(viewMonth, direction * 12));
@@ -2377,19 +2510,24 @@ function YearPickerGrid({ classNames, ...props }) {
2377
2510
  [adapter, viewMonth, ctx]
2378
2511
  );
2379
2512
  const handleYearSelect = react.useCallback(
2380
- (year) => {
2513
+ (indexInDecade) => {
2514
+ if (yearDisabledFlags[indexInDecade]) return;
2515
+ const year = decadeStart + indexInDecade;
2381
2516
  const target = new Date(Date.UTC(year, 0, 1)).toISOString();
2382
2517
  ctx.selectDate(target);
2383
2518
  },
2384
- [ctx]
2385
- );
2386
- const years = Array.from({ length: 12 }, (_, i) => {
2387
- const year = decadeStart + i;
2388
- return {
2389
- value: year,
2390
- isSelected: year === valueYear,
2391
- isCurrent: year === todayYear
2392
- };
2519
+ [ctx, decadeStart, yearDisabledFlags]
2520
+ );
2521
+ const naturalIndex = valueYear !== null && valueYear >= decadeStart && valueYear <= decadeStart + 11 ? valueYear - decadeStart : currentYear - decadeStart;
2522
+ const firstEnabled = yearDisabledFlags.findIndex((d) => !d);
2523
+ const initialIndex = yearDisabledFlags[naturalIndex] ? firstEnabled === -1 ? naturalIndex : firstEnabled : naturalIndex;
2524
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
2525
+ initialIndex,
2526
+ disabledFlags: yearDisabledFlags,
2527
+ onSelect: handleYearSelect,
2528
+ onPageUp: () => navigateDecade(-1),
2529
+ onPageDown: () => navigateDecade(1),
2530
+ onEscape: ctx.close
2393
2531
  });
2394
2532
  const rangeLabel = `${decadeStart}\u2013${decadeStart + 11}`;
2395
2533
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.root, ...props, children: [
@@ -2416,37 +2554,58 @@ function YearPickerGrid({ classNames, ...props }) {
2416
2554
  }
2417
2555
  )
2418
2556
  ] }),
2419
- /* @__PURE__ */ jsxRuntime.jsx("div", { role: "grid", "aria-label": rangeLabel, className: classNames?.grid, children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsxRuntime.jsx(
2557
+ /* @__PURE__ */ jsxRuntime.jsx(
2420
2558
  "div",
2421
2559
  {
2422
- role: "row",
2423
- className: classNames?.gridRow,
2424
- style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2425
- children: years.slice(rowIndex * 3, rowIndex * 3 + 3).map((y) => {
2426
- const yearClass = [
2427
- classNames?.year,
2428
- y.isSelected && classNames?.yearSelected,
2429
- y.isCurrent && classNames?.yearCurrent
2430
- ].filter(Boolean).join(" ") || void 0;
2431
- return /* @__PURE__ */ jsxRuntime.jsx(
2432
- "button",
2433
- {
2434
- type: "button",
2435
- role: "gridcell",
2436
- "aria-selected": y.isSelected || void 0,
2437
- "aria-current": y.isCurrent ? "date" : void 0,
2438
- "data-selected": y.isSelected || void 0,
2439
- "data-current": y.isCurrent || void 0,
2440
- className: yearClass,
2441
- onClick: () => handleYearSelect(y.value),
2442
- children: y.value
2443
- },
2444
- y.value
2445
- );
2446
- })
2447
- },
2448
- rowIndex
2449
- )) })
2560
+ ref: gridRef,
2561
+ role: "grid",
2562
+ "aria-label": rangeLabel,
2563
+ className: classNames?.grid,
2564
+ onKeyDown: handleKeyDown,
2565
+ children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsxRuntime.jsx(
2566
+ "div",
2567
+ {
2568
+ role: "row",
2569
+ className: classNames?.gridRow,
2570
+ style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2571
+ children: Array.from({ length: 3 }, (_2, col) => {
2572
+ const i = rowIndex * 3 + col;
2573
+ const year = decadeStart + i;
2574
+ const isSelected = year === valueYear;
2575
+ const isCurrent = year === todayYear;
2576
+ const isFocused = i === focusedIndex;
2577
+ const isDisabled = yearDisabledFlags[i] ?? false;
2578
+ const cls = [
2579
+ classNames?.year,
2580
+ isSelected && classNames?.yearSelected,
2581
+ isCurrent && classNames?.yearCurrent,
2582
+ isDisabled && classNames?.yearDisabled
2583
+ ].filter(Boolean).join(" ") || void 0;
2584
+ return /* @__PURE__ */ jsxRuntime.jsx(
2585
+ "button",
2586
+ {
2587
+ type: "button",
2588
+ role: "gridcell",
2589
+ tabIndex: isFocused ? 0 : -1,
2590
+ disabled: isDisabled,
2591
+ "aria-selected": isSelected || void 0,
2592
+ "aria-disabled": isDisabled || void 0,
2593
+ "aria-current": isCurrent ? "date" : void 0,
2594
+ "data-selected": isSelected || void 0,
2595
+ "data-current": isCurrent || void 0,
2596
+ "data-focused": isFocused || void 0,
2597
+ className: cls,
2598
+ onClick: () => handleYearSelect(i),
2599
+ children: year
2600
+ },
2601
+ i
2602
+ );
2603
+ })
2604
+ },
2605
+ rowIndex
2606
+ ))
2607
+ }
2608
+ )
2450
2609
  ] });
2451
2610
  }
2452
2611