@kalyx/react 1.0.0-rc.1 → 1.0.0-rc.10

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
@@ -1,8 +1,9 @@
1
- import { createContext, forwardRef, useState, useCallback, useContext, useId, useRef, useMemo, useEffect } from 'react';
2
- import { parseInputValue, formatTimeString, parseTimeString, getTimeInTimezone, getTime, DateFnsAdapter, DEFAULT_DATEPICKER_LABELS, civilMidnightFromUtcDay, getMonthName, getWeekdayNames, getCalendarDays, formatMonthYear, isDateDisabled, DEFAULT_RANGEPICKER_LABELS, DEFAULT_TIMEPICKER_LABELS, setTimeInTimezone, setTime, to12Hour, to24Hour, generateMinutes, generateHours, formatFullDate } from '@kalyx/core';
3
- export { DateFnsAdapter } from '@kalyx/core';
4
- import { jsx, jsxs } from 'react/jsx-runtime';
5
- import { useFloating, autoUpdate, offset, flip, shift } from '@floating-ui/react';
1
+ "use client";
2
+ import { createContext, forwardRef, useState, useRef, useCallback, useContext, useId, useMemo, useEffect } from 'react';
3
+ import { parseInputValue, formatTimeString, parseTimeString, getTimeInTimezone, getTime, DateFnsAdapter, DEFAULT_DATEPICKER_LABELS, civilMidnightFromUtcDay, getMonthName, getWeekdayNames, getCalendarDays, formatMonthYear, isDateDisabled, getISOWeekNumber, DEFAULT_RANGEPICKER_LABELS, DEFAULT_TIMEPICKER_LABELS, setTimeInTimezone, setTime, to12Hour, to24Hour, generateMinutes, generateHours, formatFullDate } from '@kalyx/core';
4
+ export { DEFAULT_DATEPICKER_LABELS, DEFAULT_DATETIMEPICKER_LABELS, DEFAULT_RANGEPICKER_LABELS, DEFAULT_TIMEPICKER_LABELS, DateFnsAdapter } from '@kalyx/core';
5
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
+ import { offset, flip, shift, useFloating, autoUpdate } from '@floating-ui/react';
6
7
 
7
8
  // src/components/DatePicker/Root.tsx
8
9
  var DatePickerContext = createContext(null);
@@ -56,10 +57,10 @@ function DatePickerRoot({
56
57
  const currentValue = isControlled ? controlledValue ?? null : uncontrolledValue;
57
58
  const [isOpen, setIsOpen] = useState(false);
58
59
  const [viewMonth, setViewMonth] = useState(
59
- currentValue ?? adapter.today(displayTimezone)
60
+ () => currentValue ?? adapter.today(displayTimezone)
60
61
  );
61
62
  const [focusedDate, setFocusedDate] = useState(
62
- currentValue ?? adapter.today(displayTimezone)
63
+ () => currentValue ?? adapter.today(displayTimezone)
63
64
  );
64
65
  useChangeEffect(isOpen, onOpenChange);
65
66
  const viewMonthStart = useMemo(() => adapter.startOfMonth(viewMonth), [viewMonth, adapter]);
@@ -150,10 +151,11 @@ function DatePickerRoot({
150
151
  return /* @__PURE__ */ jsx(DatePickerContext.Provider, { value: contextValue, children });
151
152
  }
152
153
  var DatePickerInput = forwardRef(
153
- function DatePickerInput2({ format: formatProp, onClick, onBlur, onKeyDown, ...props }, ref) {
154
+ function DatePickerInput2({ format: formatProp, name, onClick, onBlur, onKeyDown, ...props }, ref) {
154
155
  const ctx = useDatePickerContext("DatePicker.Input");
155
156
  const displayFormat = formatProp ?? ctx.displayFormat;
156
157
  const [inputText, setInputText] = useState(null);
158
+ const isComposingRef = useRef(false);
157
159
  let formattedValue = "";
158
160
  if (ctx.value) {
159
161
  try {
@@ -170,47 +172,62 @@ var DatePickerInput = forwardRef(
170
172
  },
171
173
  [ctx, onClick]
172
174
  );
175
+ const commitText = useCallback(
176
+ (text) => {
177
+ if (!text) {
178
+ ctx.selectDate(null);
179
+ setInputText(null);
180
+ return true;
181
+ }
182
+ const parsed = parseInputValue(text, ctx.adapter);
183
+ if (parsed) {
184
+ ctx.selectDate(parsed);
185
+ setInputText(null);
186
+ return true;
187
+ }
188
+ return false;
189
+ },
190
+ [ctx]
191
+ );
173
192
  const handleBlur = useCallback(
174
193
  (e) => {
175
194
  if (inputText !== null) {
176
- const parsed = parseInputValue(inputText, ctx.adapter);
177
- if (parsed) {
178
- ctx.selectDate(parsed);
179
- }
195
+ commitText(inputText);
180
196
  setInputText(null);
181
197
  }
182
198
  onBlur?.(e);
183
199
  },
184
- [inputText, displayFormat, ctx, onBlur]
200
+ [inputText, commitText, onBlur]
185
201
  );
186
202
  const handleChange = useCallback(
187
203
  (e) => {
188
204
  const text = e.target.value;
189
205
  setInputText(text);
190
- if (!text) {
191
- ctx.selectDate(null);
192
- setInputText(null);
193
- return;
194
- }
195
- const parsed = parseInputValue(text, ctx.adapter);
196
- if (parsed) {
197
- ctx.selectDate(parsed);
198
- setInputText(null);
199
- }
206
+ if (isComposingRef.current) return;
207
+ commitText(text);
200
208
  },
201
- [displayFormat, ctx]
209
+ [commitText]
210
+ );
211
+ const handleCompositionStart = useCallback(() => {
212
+ isComposingRef.current = true;
213
+ }, []);
214
+ const handleCompositionEnd = useCallback(
215
+ (e) => {
216
+ isComposingRef.current = false;
217
+ commitText(e.target.value);
218
+ },
219
+ [commitText]
202
220
  );
203
221
  const handleKeyDown = useCallback(
204
222
  (e) => {
205
223
  if (e.key === "Escape") {
206
224
  ctx.close();
207
225
  } else if (e.key === "Enter") {
226
+ if (ctx.isOpen) e.preventDefault();
208
227
  if (inputText !== null) {
209
- const parsed = parseInputValue(inputText, ctx.adapter);
210
- if (parsed) {
211
- ctx.selectDate(parsed);
212
- setInputText(null);
213
- }
228
+ commitText(inputText);
229
+ } else if (ctx.isOpen) {
230
+ ctx.selectDate(ctx.focusedDate);
214
231
  }
215
232
  } else if (e.key === "ArrowDown" && !ctx.isOpen) {
216
233
  e.preventDefault();
@@ -218,36 +235,42 @@ var DatePickerInput = forwardRef(
218
235
  }
219
236
  onKeyDown?.(e);
220
237
  },
221
- [ctx, inputText, displayFormat, onKeyDown]
238
+ [ctx, inputText, commitText, onKeyDown]
222
239
  );
223
240
  const calendarId = `${ctx.pickerId}-calendar`;
224
- return /* @__PURE__ */ jsx(
225
- "input",
226
- {
227
- ref: (node) => {
228
- ctx.referenceRef.current = node;
229
- if (typeof ref === "function") ref(node);
230
- else if (ref) ref.current = node;
231
- },
232
- type: "text",
233
- role: "combobox",
234
- "aria-expanded": ctx.isOpen,
235
- "aria-haspopup": "dialog",
236
- "aria-controls": ctx.isOpen ? calendarId : void 0,
237
- "aria-autocomplete": "none",
238
- autoComplete: "off",
239
- value: displayValue,
240
- disabled: ctx.isDisabled || props.disabled,
241
- readOnly: ctx.isReadOnly,
242
- onChange: handleChange,
243
- onClick: handleClick,
244
- onBlur: handleBlur,
245
- onKeyDown: handleKeyDown,
246
- ...props
247
- }
248
- );
241
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
242
+ /* @__PURE__ */ jsx(
243
+ "input",
244
+ {
245
+ ref: (node) => {
246
+ ctx.referenceRef.current = node;
247
+ if (typeof ref === "function") ref(node);
248
+ else if (ref) ref.current = node;
249
+ },
250
+ type: "text",
251
+ role: "combobox",
252
+ "aria-expanded": ctx.isOpen,
253
+ "aria-haspopup": "dialog",
254
+ "aria-controls": ctx.isOpen ? calendarId : void 0,
255
+ "aria-autocomplete": "none",
256
+ autoComplete: "off",
257
+ value: displayValue,
258
+ disabled: ctx.isDisabled || props.disabled,
259
+ readOnly: ctx.isReadOnly,
260
+ onChange: handleChange,
261
+ onClick: handleClick,
262
+ onBlur: handleBlur,
263
+ onKeyDown: handleKeyDown,
264
+ onCompositionStart: handleCompositionStart,
265
+ onCompositionEnd: handleCompositionEnd,
266
+ ...props
267
+ }
268
+ ),
269
+ name ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value: ctx.value ?? "" }) : null
270
+ ] });
249
271
  }
250
272
  );
273
+ DatePickerInput.displayName = "DatePicker.Input";
251
274
  var DatePickerTrigger = forwardRef(
252
275
  function DatePickerTrigger2({ onClick, children, ...props }, ref) {
253
276
  const ctx = useDatePickerContext("DatePicker.Trigger");
@@ -271,6 +294,7 @@ var DatePickerTrigger = forwardRef(
271
294
  tabIndex: 0,
272
295
  "aria-label": ctx.isOpen ? ctx.labels.triggerClose : ctx.labels.triggerOpen,
273
296
  "aria-expanded": ctx.isOpen,
297
+ "aria-haspopup": "dialog",
274
298
  "aria-controls": ctx.isOpen ? calendarId : void 0,
275
299
  disabled: ctx.isDisabled || props.disabled,
276
300
  onClick: handleClick,
@@ -301,6 +325,8 @@ var DatePickerTrigger = forwardRef(
301
325
  );
302
326
  }
303
327
  );
328
+ DatePickerTrigger.displayName = "DatePicker.Trigger";
329
+ var POPOVER_MIDDLEWARE = [offset(4), flip(), shift({ padding: 8 })];
304
330
  function usePopover({
305
331
  isOpen,
306
332
  close,
@@ -312,7 +338,7 @@ function usePopover({
312
338
  const { refs, floatingStyles, isPositioned } = useFloating({
313
339
  open: isOpen,
314
340
  placement,
315
- middleware: [offset(4), flip(), shift({ padding: 8 })],
341
+ middleware: POPOVER_MIDDLEWARE,
316
342
  whileElementsMounted: autoUpdate
317
343
  });
318
344
  useEffect(() => {
@@ -359,6 +385,24 @@ function usePopover({
359
385
  document.addEventListener("keydown", handleKeyDown);
360
386
  return () => document.removeEventListener("keydown", handleKeyDown);
361
387
  }, [isOpen, close]);
388
+ useEffect(() => {
389
+ if (!isOpen) return;
390
+ function handleFocusOut(e) {
391
+ const next = e.relatedTarget;
392
+ const floating = floatingRef.current;
393
+ const reference = referenceRef.current;
394
+ if (!next) return;
395
+ const insideFloating = floating?.contains(next) ?? false;
396
+ const insideReference = reference?.contains(next) ?? false;
397
+ if (!insideFloating && !insideReference) {
398
+ close();
399
+ }
400
+ }
401
+ const node = floatingRef.current;
402
+ if (!node) return;
403
+ node.addEventListener("focusout", handleFocusOut);
404
+ return () => node.removeEventListener("focusout", handleFocusOut);
405
+ }, [isOpen, close, referenceRef]);
362
406
  const setFloatingRef = useCallback(
363
407
  (node) => {
364
408
  floatingRef.current = node;
@@ -420,20 +464,36 @@ var srOnly = {
420
464
  function DatePickerCalendar({
421
465
  classNames,
422
466
  onTitleClick,
467
+ showWeekNumber = false,
468
+ fixedWeeks = false,
423
469
  ...props
424
470
  }) {
425
471
  const ctx = useDatePickerContext("DatePicker.Calendar");
426
472
  const gridRef = useRef(null);
427
473
  const [announcement, setAnnouncement] = useState("");
428
474
  const { adapter, viewMonth, focusedDate, weekStartsOn, disabled, locale, displayTimezone } = ctx;
429
- const weekdays = getWeekdayNames(locale, weekStartsOn);
430
- const weeks = getCalendarDays(viewMonth, adapter, {
431
- weekStartsOn,
432
- selected: ctx.value,
433
- focusedDate,
434
- disabled,
435
- timezone: displayTimezone
436
- });
475
+ const weekdays = useMemo(() => getWeekdayNames(locale, weekStartsOn), [locale, weekStartsOn]);
476
+ const weeks = useMemo(
477
+ () => getCalendarDays(viewMonth, adapter, {
478
+ weekStartsOn,
479
+ selected: ctx.value,
480
+ focusedDate,
481
+ disabled,
482
+ timezone: displayTimezone,
483
+ fixedWeeks
484
+ }),
485
+ [
486
+ viewMonth,
487
+ adapter,
488
+ weekStartsOn,
489
+ ctx.value,
490
+ focusedDate,
491
+ disabled,
492
+ displayTimezone,
493
+ fixedWeeks
494
+ ]
495
+ );
496
+ const thursdayIndex = weekStartsOn === 0 ? 4 : 3;
437
497
  const year = adapter.getYear(viewMonth);
438
498
  const month = adapter.getMonth(viewMonth);
439
499
  const title = formatMonthYear(year, month, locale);
@@ -513,6 +573,15 @@ function DatePickerCalendar({
513
573
  }
514
574
  if (newFocused) {
515
575
  e.preventDefault();
576
+ const skipStep = e.key === "ArrowLeft" || e.key === "ArrowUp" || e.key === "PageUp" || e.key === "Home" ? -1 : 1;
577
+ let attempts = 0;
578
+ while (isDateDisabled(newFocused, disabled, adapter) && attempts < 42) {
579
+ newFocused = adapter.addDays(newFocused, skipStep);
580
+ attempts++;
581
+ }
582
+ if (attempts >= 42) {
583
+ return;
584
+ }
516
585
  ctx.setFocusedDate(newFocused);
517
586
  if (!adapter.isSameMonth(newFocused, viewMonth)) {
518
587
  ctx.setViewMonth(newFocused);
@@ -560,62 +629,170 @@ function DatePickerCalendar({
560
629
  ref: gridRef,
561
630
  role: "grid",
562
631
  "aria-label": title,
632
+ "aria-rowcount": weeks.length + 1,
633
+ "aria-colcount": 7,
563
634
  className: classNames?.grid,
564
635
  onKeyDown: handleKeyDown,
565
636
  children: [
566
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { role: "row", children: weekdays.map((day) => /* @__PURE__ */ jsx(
567
- "th",
568
- {
569
- role: "columnheader",
570
- abbr: day.full,
571
- scope: "col",
572
- className: classNames?.weekdayHeader,
573
- children: day.short
574
- },
575
- day.short
576
- )) }) }),
577
- /* @__PURE__ */ jsx("tbody", { children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsx("tr", { role: "row", className: classNames?.gridRow, children: week.map((day) => {
578
- const dayClasses = [
579
- classNames?.day,
580
- day.isSelected && classNames?.daySelected,
581
- day.isToday && classNames?.dayToday,
582
- day.isDisabled && classNames?.dayDisabled,
583
- !day.isCurrentMonth && classNames?.dayOutsideMonth
584
- ].filter(Boolean).join(" ") || void 0;
585
- return /* @__PURE__ */ jsx(
586
- "td",
637
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { role: "row", "aria-rowindex": 1, children: [
638
+ showWeekNumber ? /* @__PURE__ */ jsx("th", { scope: "col", "aria-hidden": "true", className: classNames?.weekNumberHeader, children: "#" }) : null,
639
+ weekdays.map((day, colIndex) => /* @__PURE__ */ jsx(
640
+ "th",
587
641
  {
588
- role: "gridcell",
589
- "aria-selected": day.isSelected || void 0,
590
- "aria-disabled": day.isDisabled || void 0,
591
- "aria-current": day.isToday ? "date" : void 0,
592
- className: classNames?.gridCell,
593
- children: /* @__PURE__ */ jsx(
594
- "button",
642
+ role: "columnheader",
643
+ abbr: day.full,
644
+ scope: "col",
645
+ "aria-colindex": colIndex + 1,
646
+ className: classNames?.weekdayHeader,
647
+ children: day.short
648
+ },
649
+ day.short
650
+ ))
651
+ ] }) }),
652
+ /* @__PURE__ */ jsx("tbody", { children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsxs(
653
+ "tr",
654
+ {
655
+ role: "row",
656
+ "aria-rowindex": weekIndex + 2,
657
+ className: classNames?.gridRow,
658
+ children: [
659
+ showWeekNumber ? /* @__PURE__ */ jsx(
660
+ "th",
595
661
  {
596
- type: "button",
597
- tabIndex: day.isFocused ? 0 : -1,
598
- disabled: day.isDisabled,
599
- "data-focused": day.isFocused || void 0,
600
- "data-selected": day.isSelected || void 0,
601
- "data-today": day.isToday || void 0,
602
- "data-outside-month": !day.isCurrentMonth || void 0,
603
- className: dayClasses,
604
- onClick: () => handleDayClick(day),
605
- "aria-label": safeFormatFullDate(day.isoString, locale),
606
- children: day.dayNumber
662
+ scope: "row",
663
+ "aria-hidden": "true",
664
+ className: classNames?.weekNumber,
665
+ "data-week-number": true,
666
+ children: getISOWeekNumber(week[thursdayIndex].isoString)
607
667
  }
608
- )
609
- },
610
- day.isoString
611
- );
612
- }) }, weekIndex)) })
668
+ ) : null,
669
+ week.map((day, colIndex) => {
670
+ const dayClasses = [
671
+ classNames?.day,
672
+ day.isSelected && classNames?.daySelected,
673
+ day.isToday && classNames?.dayToday,
674
+ day.isDisabled && classNames?.dayDisabled,
675
+ !day.isCurrentMonth && classNames?.dayOutsideMonth
676
+ ].filter(Boolean).join(" ") || void 0;
677
+ return /* @__PURE__ */ jsx(
678
+ "td",
679
+ {
680
+ role: "gridcell",
681
+ "aria-colindex": colIndex + 1,
682
+ "aria-selected": day.isSelected || void 0,
683
+ "aria-disabled": day.isDisabled || void 0,
684
+ "aria-current": day.isToday ? "date" : void 0,
685
+ className: classNames?.gridCell,
686
+ children: /* @__PURE__ */ jsx(
687
+ "button",
688
+ {
689
+ type: "button",
690
+ tabIndex: day.isFocused ? 0 : -1,
691
+ disabled: day.isDisabled,
692
+ "data-focused": day.isFocused || void 0,
693
+ "data-selected": day.isSelected || void 0,
694
+ "data-today": day.isToday || void 0,
695
+ "data-outside-month": !day.isCurrentMonth || void 0,
696
+ className: dayClasses,
697
+ onClick: () => handleDayClick(day),
698
+ "aria-label": safeFormatFullDate(day.isoString, locale),
699
+ children: day.dayNumber
700
+ }
701
+ )
702
+ },
703
+ day.isoString
704
+ );
705
+ })
706
+ ]
707
+ },
708
+ weekIndex
709
+ )) })
613
710
  ]
614
711
  }
615
712
  ),
616
713
  /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", style: srOnly, children: announcement })
617
714
  ] });
618
715
  }
716
+ function isRangeFullyDisabled(start, end, rules, adapter) {
717
+ for (const rule of rules) {
718
+ if ("before" in rule && adapter.isBefore(end, rule.before)) return true;
719
+ if ("after" in rule && adapter.isAfter(start, rule.after)) return true;
720
+ }
721
+ return false;
722
+ }
723
+ function useGridState(opts) {
724
+ const { initialIndex, disabledFlags, onSelect, onPageUp, onPageDown, onEscape } = opts;
725
+ const gridRef = useRef(null);
726
+ const [focusedIndex, setFocusedIndex] = useState(initialIndex);
727
+ const handleKeyDown = (e) => {
728
+ let next = null;
729
+ let step = 1;
730
+ switch (e.key) {
731
+ case "ArrowLeft":
732
+ next = Math.max(0, focusedIndex - 1);
733
+ step = -1;
734
+ break;
735
+ case "ArrowRight":
736
+ next = Math.min(11, focusedIndex + 1);
737
+ break;
738
+ case "ArrowUp":
739
+ next = Math.max(0, focusedIndex - 3);
740
+ step = -1;
741
+ break;
742
+ case "ArrowDown":
743
+ next = Math.min(11, focusedIndex + 3);
744
+ break;
745
+ case "Home":
746
+ next = focusedIndex - focusedIndex % 3;
747
+ step = -1;
748
+ break;
749
+ case "End":
750
+ next = focusedIndex - focusedIndex % 3 + 2;
751
+ break;
752
+ case "PageUp":
753
+ e.preventDefault();
754
+ onPageUp();
755
+ return;
756
+ case "PageDown":
757
+ e.preventDefault();
758
+ onPageDown();
759
+ return;
760
+ case "Enter":
761
+ case " ":
762
+ e.preventDefault();
763
+ onSelect(focusedIndex);
764
+ return;
765
+ case "Escape":
766
+ onEscape();
767
+ return;
768
+ default:
769
+ return;
770
+ }
771
+ if (next === null) return;
772
+ e.preventDefault();
773
+ if (disabledFlags) {
774
+ let attempts = 0;
775
+ while (next >= 0 && next < 12 && disabledFlags[next] && attempts < 12) {
776
+ next += step;
777
+ attempts++;
778
+ }
779
+ if (next < 0 || next >= 12 || disabledFlags[next]) return;
780
+ }
781
+ if (next !== focusedIndex) setFocusedIndex(next);
782
+ };
783
+ useEffect(() => {
784
+ if (!disabledFlags || !disabledFlags[focusedIndex]) return;
785
+ const firstEnabled = disabledFlags.findIndex((d) => !d);
786
+ if (firstEnabled !== -1 && firstEnabled !== focusedIndex) {
787
+ setFocusedIndex(firstEnabled);
788
+ }
789
+ }, [disabledFlags, focusedIndex]);
790
+ useEffect(() => {
791
+ const btn = gridRef.current?.querySelector('[data-focused="true"]');
792
+ btn?.focus({ preventScroll: true });
793
+ }, [focusedIndex]);
794
+ return { gridRef, focusedIndex, handleKeyDown };
795
+ }
619
796
  function DatePickerMonthGrid({
620
797
  classNames,
621
798
  onSelect,
@@ -623,15 +800,18 @@ function DatePickerMonthGrid({
623
800
  ...props
624
801
  }) {
625
802
  const ctx = useDatePickerContext("DatePicker.MonthGrid");
626
- const { adapter, viewMonth, locale } = ctx;
803
+ const { adapter, viewMonth, locale, displayTimezone } = ctx;
627
804
  const currentYear = adapter.getYear(viewMonth);
628
805
  const currentMonth = adapter.getMonth(viewMonth);
629
- const todayMonth = adapter.getMonth(adapter.today());
630
- const todayYear = adapter.getYear(adapter.today());
806
+ const [today, setToday] = useState(null);
807
+ useEffect(() => {
808
+ setToday(adapter.today(displayTimezone));
809
+ }, [adapter, displayTimezone]);
810
+ const todayMonth = today !== null ? adapter.getMonth(today) : -1;
811
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
631
812
  const navigateYear = useCallback(
632
813
  (direction) => {
633
- const newDate = adapter.addYears(viewMonth, direction);
634
- ctx.setViewMonth(newDate);
814
+ ctx.setViewMonth(adapter.addYears(viewMonth, direction));
635
815
  },
636
816
  [adapter, viewMonth, ctx]
637
817
  );
@@ -644,12 +824,13 @@ function DatePickerMonthGrid({
644
824
  },
645
825
  [currentYear, ctx, onSelect]
646
826
  );
647
- const months = Array.from({ length: 12 }, (_, i) => ({
648
- index: i,
649
- name: getMonthName(i, locale),
650
- isSelected: i === currentMonth,
651
- isCurrent: i === todayMonth && currentYear === todayYear
652
- }));
827
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
828
+ initialIndex: currentMonth,
829
+ onSelect: handleMonthSelect,
830
+ onPageUp: () => navigateYear(-1),
831
+ onPageDown: () => navigateYear(1),
832
+ onEscape: ctx.close
833
+ });
653
834
  return /* @__PURE__ */ jsxs("div", { className: classNames?.root, ...props, children: [
654
835
  /* @__PURE__ */ jsxs("div", { className: classNames?.header, children: [
655
836
  /* @__PURE__ */ jsx(
@@ -677,30 +858,37 @@ function DatePickerMonthGrid({
677
858
  /* @__PURE__ */ jsx(
678
859
  "div",
679
860
  {
861
+ ref: gridRef,
680
862
  role: "grid",
681
863
  "aria-label": `${currentYear} months`,
682
864
  className: classNames?.grid,
683
865
  style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
684
- children: months.map((m) => {
685
- const monthClass = [
866
+ onKeyDown: handleKeyDown,
867
+ children: Array.from({ length: 12 }, (_, i) => {
868
+ const isSelected = i === currentMonth;
869
+ const isCurrent = i === todayMonth && currentYear === todayYear;
870
+ const isFocused = i === focusedIndex;
871
+ const cls = [
686
872
  classNames?.month,
687
- m.isSelected && classNames?.monthSelected,
688
- m.isCurrent && classNames?.monthCurrent
873
+ isSelected && classNames?.monthSelected,
874
+ isCurrent && classNames?.monthCurrent
689
875
  ].filter(Boolean).join(" ") || void 0;
690
876
  return /* @__PURE__ */ jsx(
691
877
  "button",
692
878
  {
693
879
  type: "button",
694
880
  role: "gridcell",
695
- "aria-selected": m.isSelected || void 0,
696
- "aria-current": m.isCurrent ? "date" : void 0,
697
- "data-selected": m.isSelected || void 0,
698
- "data-current": m.isCurrent || void 0,
699
- className: monthClass,
700
- onClick: () => handleMonthSelect(m.index),
701
- children: m.name
881
+ tabIndex: isFocused ? 0 : -1,
882
+ "aria-selected": isSelected || void 0,
883
+ "aria-current": isCurrent ? "date" : void 0,
884
+ "data-selected": isSelected || void 0,
885
+ "data-current": isCurrent || void 0,
886
+ "data-focused": isFocused || void 0,
887
+ className: cls,
888
+ onClick: () => handleMonthSelect(i),
889
+ children: getMonthName(i, locale)
702
890
  },
703
- m.index
891
+ i
704
892
  );
705
893
  })
706
894
  }
@@ -709,38 +897,38 @@ function DatePickerMonthGrid({
709
897
  }
710
898
  function DatePickerYearGrid({ classNames, onSelect, ...props }) {
711
899
  const ctx = useDatePickerContext("DatePicker.YearGrid");
712
- const { adapter, viewMonth } = ctx;
900
+ const { adapter, viewMonth, displayTimezone } = ctx;
713
901
  const currentYear = adapter.getYear(viewMonth);
714
- const todayYear = adapter.getYear(adapter.today());
902
+ const [today, setToday] = useState(null);
903
+ useEffect(() => {
904
+ setToday(adapter.today(displayTimezone));
905
+ }, [adapter, displayTimezone]);
906
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
715
907
  const decadeStart = currentYear - currentYear % 12;
716
908
  const navigateDecade = useCallback(
717
909
  (direction) => {
718
- const newDate = adapter.addYears(viewMonth, direction * 12);
719
- ctx.setViewMonth(newDate);
910
+ ctx.setViewMonth(adapter.addYears(viewMonth, direction * 12));
720
911
  },
721
912
  [adapter, viewMonth, ctx]
722
913
  );
723
914
  const handleYearSelect = useCallback(
724
- (year) => {
915
+ (indexInDecade) => {
916
+ const year = decadeStart + indexInDecade;
725
917
  const currentMonth = adapter.getMonth(viewMonth);
726
918
  const target = new Date(Date.UTC(year, currentMonth, 1)).toISOString();
727
919
  ctx.setViewMonth(target);
728
920
  ctx.setFocusedDate(target);
729
921
  onSelect?.();
730
922
  },
731
- [adapter, viewMonth, ctx, onSelect]
732
- );
733
- const years = useMemo(
734
- () => Array.from({ length: 12 }, (_, i) => {
735
- const year = decadeStart + i;
736
- return {
737
- value: year,
738
- isSelected: year === currentYear,
739
- isCurrent: year === todayYear
740
- };
741
- }),
742
- [decadeStart, currentYear, todayYear]
743
- );
923
+ [adapter, viewMonth, ctx, onSelect, decadeStart]
924
+ );
925
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
926
+ initialIndex: currentYear - decadeStart,
927
+ onSelect: handleYearSelect,
928
+ onPageUp: () => navigateDecade(-1),
929
+ onPageDown: () => navigateDecade(1),
930
+ onEscape: ctx.close
931
+ });
744
932
  const rangeLabel = `${decadeStart}\u2013${decadeStart + 11}`;
745
933
  return /* @__PURE__ */ jsxs("div", { className: classNames?.root, ...props, children: [
746
934
  /* @__PURE__ */ jsxs("div", { className: classNames?.header, children: [
@@ -769,30 +957,38 @@ function DatePickerYearGrid({ classNames, onSelect, ...props }) {
769
957
  /* @__PURE__ */ jsx(
770
958
  "div",
771
959
  {
960
+ ref: gridRef,
772
961
  role: "grid",
773
962
  "aria-label": rangeLabel,
774
963
  className: classNames?.grid,
775
964
  style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
776
- children: years.map((y) => {
777
- const yearClass = [
965
+ onKeyDown: handleKeyDown,
966
+ children: Array.from({ length: 12 }, (_, i) => {
967
+ const year = decadeStart + i;
968
+ const isSelected = year === currentYear;
969
+ const isCurrent = year === todayYear;
970
+ const isFocused = i === focusedIndex;
971
+ const cls = [
778
972
  classNames?.year,
779
- y.isSelected && classNames?.yearSelected,
780
- y.isCurrent && classNames?.yearCurrent
973
+ isSelected && classNames?.yearSelected,
974
+ isCurrent && classNames?.yearCurrent
781
975
  ].filter(Boolean).join(" ") || void 0;
782
976
  return /* @__PURE__ */ jsx(
783
977
  "button",
784
978
  {
785
979
  type: "button",
786
980
  role: "gridcell",
787
- "aria-selected": y.isSelected || void 0,
788
- "aria-current": y.isCurrent ? "date" : void 0,
789
- "data-selected": y.isSelected || void 0,
790
- "data-current": y.isCurrent || void 0,
791
- className: yearClass,
792
- onClick: () => handleYearSelect(y.value),
793
- children: y.value
981
+ tabIndex: isFocused ? 0 : -1,
982
+ "aria-selected": isSelected || void 0,
983
+ "aria-current": isCurrent ? "date" : void 0,
984
+ "data-selected": isSelected || void 0,
985
+ "data-current": isCurrent || void 0,
986
+ "data-focused": isFocused || void 0,
987
+ className: cls,
988
+ onClick: () => handleYearSelect(i),
989
+ children: year
794
990
  },
795
- y.value
991
+ i
796
992
  );
797
993
  })
798
994
  }
@@ -865,8 +1061,7 @@ function DatePickerPreset({
865
1061
  "button",
866
1062
  {
867
1063
  type: "button",
868
- role: "option",
869
- "aria-selected": isActive,
1064
+ "aria-pressed": isActive,
870
1065
  "data-active": isActive || void 0,
871
1066
  disabled: ctx.isDisabled,
872
1067
  onClick: handleClick,
@@ -930,10 +1125,10 @@ function RangePickerRoot({
930
1125
  const [selectingTarget, setSelectingTarget] = useState("start");
931
1126
  const [hoverDate, setHoverDate] = useState(null);
932
1127
  const [viewMonth, setViewMonth] = useState(
933
- currentValue.start ?? adapter.today(displayTimezone)
1128
+ () => currentValue.start ?? adapter.today(displayTimezone)
934
1129
  );
935
1130
  const [focusedDate, setFocusedDate] = useState(
936
- currentValue.start ?? adapter.today(displayTimezone)
1131
+ () => currentValue.start ?? adapter.today(displayTimezone)
937
1132
  );
938
1133
  useChangeEffect(isOpen, onOpenChange);
939
1134
  const viewMonthStart = useMemo(() => adapter.startOfMonth(viewMonth), [viewMonth, adapter]);
@@ -1083,6 +1278,8 @@ var RangePickerInput = forwardRef(
1083
1278
  (e) => {
1084
1279
  if (e.key === "Escape") {
1085
1280
  ctx.close();
1281
+ } else if (e.key === "Enter" && ctx.isOpen) {
1282
+ e.preventDefault();
1086
1283
  } else if (e.key === "ArrowDown" && !ctx.isOpen) {
1087
1284
  e.preventDefault();
1088
1285
  ctx.open();
@@ -1119,6 +1316,7 @@ var RangePickerInput = forwardRef(
1119
1316
  );
1120
1317
  }
1121
1318
  );
1319
+ RangePickerInput.displayName = "RangePicker.Input";
1122
1320
  function RangePickerPopover({ children, ...props }) {
1123
1321
  const ctx = useRangePickerContext("RangePicker.Popover");
1124
1322
  const calendarId = `${ctx.pickerId}-calendar`;
@@ -1168,6 +1366,8 @@ var srOnly2 = {
1168
1366
  function RangePickerCalendar({
1169
1367
  classNames,
1170
1368
  selectionMode = "range",
1369
+ showWeekNumber = false,
1370
+ fixedWeeks = false,
1171
1371
  ...props
1172
1372
  }) {
1173
1373
  const ctx = useRangePickerContext("RangePicker.Calendar");
@@ -1185,15 +1385,30 @@ function RangePickerCalendar({
1185
1385
  displayTimezone
1186
1386
  } = ctx;
1187
1387
  const { locale } = ctx;
1188
- const weekdays = getWeekdayNames(locale, weekStartsOn);
1189
- const weeks = getCalendarDays(viewMonth, adapter, {
1190
- weekStartsOn,
1191
- focusedDate,
1192
- disabled,
1193
- range: value,
1194
- rangeHover: hoverDate,
1195
- timezone: displayTimezone
1196
- });
1388
+ const weekdays = useMemo(() => getWeekdayNames(locale, weekStartsOn), [locale, weekStartsOn]);
1389
+ const weeks = useMemo(
1390
+ () => getCalendarDays(viewMonth, adapter, {
1391
+ weekStartsOn,
1392
+ focusedDate,
1393
+ disabled,
1394
+ range: value,
1395
+ rangeHover: hoverDate,
1396
+ timezone: displayTimezone,
1397
+ fixedWeeks
1398
+ }),
1399
+ [
1400
+ viewMonth,
1401
+ adapter,
1402
+ weekStartsOn,
1403
+ focusedDate,
1404
+ disabled,
1405
+ value,
1406
+ hoverDate,
1407
+ displayTimezone,
1408
+ fixedWeeks
1409
+ ]
1410
+ );
1411
+ const thursdayIndex = weekStartsOn === 0 ? 4 : 3;
1197
1412
  const year = adapter.getYear(viewMonth);
1198
1413
  const month = adapter.getMonth(viewMonth);
1199
1414
  const title = formatMonthYear(year, month, locale);
@@ -1293,6 +1508,13 @@ function RangePickerCalendar({
1293
1508
  }
1294
1509
  if (newFocused) {
1295
1510
  e.preventDefault();
1511
+ const skipStep = e.key === "ArrowLeft" || e.key === "ArrowUp" || e.key === "PageUp" || e.key === "Home" ? -1 : 1;
1512
+ let attempts = 0;
1513
+ while (isDateDisabled(newFocused, disabled, adapter) && attempts < 42) {
1514
+ newFocused = adapter.addDays(newFocused, skipStep);
1515
+ attempts++;
1516
+ }
1517
+ if (attempts >= 42) return;
1296
1518
  ctx.setFocusedDate(newFocused);
1297
1519
  if (!adapter.isSameMonth(newFocused, viewMonth)) {
1298
1520
  ctx.setViewMonth(newFocused);
@@ -1345,63 +1567,90 @@ function RangePickerCalendar({
1345
1567
  ref: gridRef,
1346
1568
  role: "grid",
1347
1569
  "aria-label": title,
1348
- "aria-multiselectable": "true",
1570
+ "aria-rowcount": weeks.length + 1,
1571
+ "aria-colcount": 7,
1349
1572
  className: classNames?.grid,
1350
1573
  onKeyDown: handleKeyDown,
1351
1574
  children: [
1352
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { role: "row", children: weekdays.map((day) => /* @__PURE__ */ jsx(
1353
- "th",
1354
- {
1355
- role: "columnheader",
1356
- abbr: day.full,
1357
- scope: "col",
1358
- className: classNames?.weekdayHeader,
1359
- children: day.short
1360
- },
1361
- day.short
1362
- )) }) }),
1363
- /* @__PURE__ */ jsx("tbody", { children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsx("tr", { role: "row", className: classNames?.gridRow, children: week.map((day) => {
1364
- const dayClasses = [
1365
- classNames?.day,
1366
- day.isRangeStart && classNames?.dayRangeStart,
1367
- day.isRangeEnd && classNames?.dayRangeEnd,
1368
- day.isInRange && classNames?.dayInRange,
1369
- day.isToday && classNames?.dayToday,
1370
- day.isDisabled && classNames?.dayDisabled,
1371
- !day.isCurrentMonth && classNames?.dayOutsideMonth
1372
- ].filter(Boolean).join(" ") || void 0;
1373
- const isSelected = selectionMode === "week" ? day.isRangeStart || day.isRangeEnd || day.isInRange : day.isRangeStart || day.isRangeEnd;
1374
- return /* @__PURE__ */ jsx(
1375
- "td",
1575
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { role: "row", "aria-rowindex": 1, children: [
1576
+ showWeekNumber ? /* @__PURE__ */ jsx("th", { scope: "col", "aria-hidden": "true", className: classNames?.weekNumberHeader, children: "#" }) : null,
1577
+ weekdays.map((day, colIndex) => /* @__PURE__ */ jsx(
1578
+ "th",
1376
1579
  {
1377
- role: "gridcell",
1378
- "aria-selected": isSelected || void 0,
1379
- "aria-disabled": day.isDisabled || void 0,
1380
- "aria-current": day.isToday ? "date" : void 0,
1381
- className: classNames?.gridCell,
1382
- children: /* @__PURE__ */ jsx(
1383
- "button",
1580
+ role: "columnheader",
1581
+ abbr: day.full,
1582
+ scope: "col",
1583
+ "aria-colindex": colIndex + 1,
1584
+ className: classNames?.weekdayHeader,
1585
+ children: day.short
1586
+ },
1587
+ day.short
1588
+ ))
1589
+ ] }) }),
1590
+ /* @__PURE__ */ jsx("tbody", { children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsxs(
1591
+ "tr",
1592
+ {
1593
+ role: "row",
1594
+ "aria-rowindex": weekIndex + 2,
1595
+ className: classNames?.gridRow,
1596
+ children: [
1597
+ showWeekNumber ? /* @__PURE__ */ jsx(
1598
+ "th",
1384
1599
  {
1385
- type: "button",
1386
- tabIndex: day.isFocused ? 0 : -1,
1387
- disabled: day.isDisabled,
1388
- "data-focused": day.isFocused || void 0,
1389
- "data-range-start": day.isRangeStart || void 0,
1390
- "data-range-end": day.isRangeEnd || void 0,
1391
- "data-in-range": day.isInRange || void 0,
1392
- "data-today": day.isToday || void 0,
1393
- "data-outside-month": !day.isCurrentMonth || void 0,
1394
- className: dayClasses,
1395
- onClick: () => handleDayClick(day),
1396
- onMouseEnter: () => handleDayMouseEnter(day),
1397
- "aria-label": safeFormatFullDate2(day.isoString, locale),
1398
- children: day.dayNumber
1600
+ scope: "row",
1601
+ "aria-hidden": "true",
1602
+ className: classNames?.weekNumber,
1603
+ "data-week-number": true,
1604
+ children: getISOWeekNumber(week[thursdayIndex].isoString)
1399
1605
  }
1400
- )
1401
- },
1402
- day.isoString
1403
- );
1404
- }) }, weekIndex)) })
1606
+ ) : null,
1607
+ week.map((day, colIndex) => {
1608
+ const dayClasses = [
1609
+ classNames?.day,
1610
+ day.isRangeStart && classNames?.dayRangeStart,
1611
+ day.isRangeEnd && classNames?.dayRangeEnd,
1612
+ day.isInRange && classNames?.dayInRange,
1613
+ day.isToday && classNames?.dayToday,
1614
+ day.isDisabled && classNames?.dayDisabled,
1615
+ !day.isCurrentMonth && classNames?.dayOutsideMonth
1616
+ ].filter(Boolean).join(" ") || void 0;
1617
+ const isSelected = selectionMode === "week" ? day.isRangeStart || day.isRangeEnd || day.isInRange : day.isRangeStart || day.isRangeEnd;
1618
+ return /* @__PURE__ */ jsx(
1619
+ "td",
1620
+ {
1621
+ role: "gridcell",
1622
+ "aria-colindex": colIndex + 1,
1623
+ "aria-selected": isSelected || void 0,
1624
+ "aria-disabled": day.isDisabled || void 0,
1625
+ "aria-current": day.isToday ? "date" : void 0,
1626
+ className: classNames?.gridCell,
1627
+ children: /* @__PURE__ */ jsx(
1628
+ "button",
1629
+ {
1630
+ type: "button",
1631
+ tabIndex: day.isFocused ? 0 : -1,
1632
+ disabled: day.isDisabled,
1633
+ "data-focused": day.isFocused || void 0,
1634
+ "data-range-start": day.isRangeStart || void 0,
1635
+ "data-range-end": day.isRangeEnd || void 0,
1636
+ "data-in-range": day.isInRange || void 0,
1637
+ "data-today": day.isToday || void 0,
1638
+ "data-outside-month": !day.isCurrentMonth || void 0,
1639
+ className: dayClasses,
1640
+ onClick: () => handleDayClick(day),
1641
+ onMouseEnter: () => handleDayMouseEnter(day),
1642
+ "aria-label": safeFormatFullDate2(day.isoString, locale),
1643
+ children: day.dayNumber
1644
+ }
1645
+ )
1646
+ },
1647
+ day.isoString
1648
+ );
1649
+ })
1650
+ ]
1651
+ },
1652
+ weekIndex
1653
+ )) })
1405
1654
  ]
1406
1655
  }
1407
1656
  ),
@@ -1463,41 +1712,33 @@ function RangePickerPreset({
1463
1712
  ...props
1464
1713
  }) {
1465
1714
  const ctx = useRangePickerContext("RangePicker.Preset");
1715
+ const resolved = useMemo(() => {
1716
+ if (directRange) return directRange;
1717
+ if (presetKey)
1718
+ return resolvePreset(presetKey, ctx.adapter.today(ctx.displayTimezone), ctx.adapter);
1719
+ return null;
1720
+ }, [directRange, presetKey, ctx.adapter, ctx.displayTimezone]);
1466
1721
  const handleClick = useCallback(
1467
1722
  (e) => {
1468
1723
  if (ctx.isDisabled || ctx.isReadOnly) return;
1469
- let resolved;
1470
- if (directRange) {
1471
- resolved = directRange;
1472
- } else if (presetKey) {
1473
- resolved = resolvePreset(presetKey, ctx.adapter.today(), ctx.adapter);
1474
- } else {
1475
- return;
1476
- }
1724
+ if (!resolved) return;
1477
1725
  ctx.setRange(resolved);
1478
1726
  ctx.close();
1479
1727
  onClick?.(e);
1480
1728
  },
1481
- [ctx, presetKey, directRange, onClick]
1729
+ [ctx, resolved, onClick]
1482
1730
  );
1483
- const isActive = (() => {
1484
- if (!ctx.value.start || !ctx.value.end) return false;
1485
- let target;
1486
- if (directRange) {
1487
- target = directRange;
1488
- } else if (presetKey) {
1489
- target = resolvePreset(presetKey, ctx.adapter.today(), ctx.adapter);
1490
- } else {
1731
+ const isActive = useMemo(() => {
1732
+ if (!ctx.value.start || !ctx.value.end || !resolved || !resolved.start || !resolved.end) {
1491
1733
  return false;
1492
1734
  }
1493
- return target.start !== null && target.end !== null && ctx.adapter.isSameDay(ctx.value.start, target.start) && ctx.adapter.isSameDay(ctx.value.end, target.end);
1494
- })();
1735
+ return ctx.adapter.isSameDay(ctx.value.start, resolved.start) && ctx.adapter.isSameDay(ctx.value.end, resolved.end);
1736
+ }, [ctx.value.start, ctx.value.end, ctx.adapter, resolved]);
1495
1737
  return /* @__PURE__ */ jsx(
1496
1738
  "button",
1497
1739
  {
1498
1740
  type: "button",
1499
- role: "option",
1500
- "aria-selected": isActive,
1741
+ "aria-pressed": isActive,
1501
1742
  "data-active": isActive || void 0,
1502
1743
  disabled: ctx.isDisabled,
1503
1744
  onClick: handleClick,
@@ -1530,9 +1771,6 @@ function useTimePickerContext(componentName) {
1530
1771
  }
1531
1772
  return context;
1532
1773
  }
1533
- function getDefaultIso() {
1534
- return DateFnsAdapter.today();
1535
- }
1536
1774
  function TimePickerRoot({
1537
1775
  value: controlledValue,
1538
1776
  defaultValue,
@@ -1543,6 +1781,7 @@ function TimePickerRoot({
1543
1781
  displayTimezone,
1544
1782
  disabled = false,
1545
1783
  readOnly = false,
1784
+ filterTime,
1546
1785
  labels: labelsProp,
1547
1786
  children
1548
1787
  }) {
@@ -1556,21 +1795,21 @@ function TimePickerRoot({
1556
1795
  defaultValue ?? null
1557
1796
  );
1558
1797
  const currentValue = isControlled ? controlledValue ?? null : uncontrolledValue;
1559
- const baseIso = currentValue ?? getDefaultIso();
1560
- const currentTime = useMemo(
1561
- () => displayTimezone ? getTimeInTimezone(baseIso, displayTimezone) : getTime(baseIso),
1562
- [baseIso, displayTimezone]
1563
- );
1798
+ const currentTime = useMemo(() => {
1799
+ if (!currentValue) return { hours: 0, minutes: 0, seconds: 0 };
1800
+ return displayTimezone ? getTimeInTimezone(currentValue, displayTimezone) : getTime(currentValue);
1801
+ }, [currentValue, displayTimezone]);
1564
1802
  const setTime$1 = useCallback(
1565
1803
  (partial) => {
1566
1804
  if (disabled || readOnly) return;
1567
- const newIso = displayTimezone ? setTimeInTimezone(baseIso, partial, displayTimezone) : setTime(baseIso, partial);
1805
+ const base = currentValue ?? DateFnsAdapter.today(displayTimezone);
1806
+ const newIso = displayTimezone ? setTimeInTimezone(base, partial, displayTimezone) : setTime(base, partial);
1568
1807
  if (!isControlled) {
1569
1808
  setUncontrolledValue(newIso);
1570
1809
  }
1571
1810
  onChange?.(newIso);
1572
1811
  },
1573
- [disabled, readOnly, baseIso, isControlled, onChange, displayTimezone]
1812
+ [disabled, readOnly, currentValue, displayTimezone, isControlled, onChange]
1574
1813
  );
1575
1814
  const contextValue = useMemo(
1576
1815
  () => ({
@@ -1584,7 +1823,8 @@ function TimePickerRoot({
1584
1823
  isReadOnly: readOnly,
1585
1824
  currentTime,
1586
1825
  pickerId,
1587
- labels: mergedLabels
1826
+ labels: mergedLabels,
1827
+ filterTime
1588
1828
  }),
1589
1829
  [
1590
1830
  currentValue,
@@ -1597,7 +1837,8 @@ function TimePickerRoot({
1597
1837
  readOnly,
1598
1838
  currentTime,
1599
1839
  pickerId,
1600
- mergedLabels
1840
+ mergedLabels,
1841
+ filterTime
1601
1842
  ]
1602
1843
  );
1603
1844
  return /* @__PURE__ */ jsx(TimePickerContext.Provider, { value: contextValue, children });
@@ -1654,6 +1895,7 @@ var TimePickerInput = forwardRef(
1654
1895
  );
1655
1896
  }
1656
1897
  );
1898
+ TimePickerInput.displayName = "TimePicker.Input";
1657
1899
  function useListboxNavigation({
1658
1900
  items,
1659
1901
  onSelect,
@@ -1701,17 +1943,41 @@ function useListboxNavigation({
1701
1943
  }
1702
1944
  function TimePickerHourList({ classNames, ...props }) {
1703
1945
  const ctx = useTimePickerContext("TimePicker.HourList");
1704
- const { format, currentTime, isDisabled, isReadOnly } = ctx;
1705
- const hours = generateHours(format);
1946
+ const { format, step, currentTime, isDisabled, isReadOnly, filterTime } = ctx;
1947
+ const hours = useMemo(() => generateHours(format), [format]);
1706
1948
  const selectedHourDisplay = format === "12h" ? to12Hour(currentTime.hours).hours12 : currentTime.hours;
1707
1949
  const currentPeriod = format === "12h" ? to12Hour(currentTime.hours).period : null;
1950
+ const fullyDisabledHours24 = useMemo(() => {
1951
+ if (!filterTime) return null;
1952
+ const disabled = /* @__PURE__ */ new Set();
1953
+ for (let h = 0; h < 24; h++) {
1954
+ let allRejected = true;
1955
+ for (let m = 0; m < 60; m += step) {
1956
+ if (!filterTime(h, m)) {
1957
+ allRejected = false;
1958
+ break;
1959
+ }
1960
+ }
1961
+ if (allRejected) disabled.add(h);
1962
+ }
1963
+ return disabled;
1964
+ }, [filterTime, step]);
1965
+ const isHourDisabled = useCallback(
1966
+ (hourDisplay) => {
1967
+ if (!fullyDisabledHours24) return false;
1968
+ const hours24 = format === "12h" && currentPeriod ? to24Hour(hourDisplay, currentPeriod) : hourDisplay;
1969
+ return fullyDisabledHours24.has(hours24);
1970
+ },
1971
+ [fullyDisabledHours24, format, currentPeriod]
1972
+ );
1708
1973
  const handleSelect = useCallback(
1709
1974
  (hourDisplay) => {
1710
1975
  if (isDisabled || isReadOnly) return;
1976
+ if (isHourDisabled(hourDisplay)) return;
1711
1977
  const hours24 = format === "12h" && currentPeriod ? to24Hour(hourDisplay, currentPeriod) : hourDisplay;
1712
1978
  ctx.setTime({ hours: hours24 });
1713
1979
  },
1714
- [format, currentPeriod, ctx, isDisabled, isReadOnly]
1980
+ [format, currentPeriod, ctx, isDisabled, isReadOnly, isHourDisabled]
1715
1981
  );
1716
1982
  const { listRef, handleKeyDown } = useListboxNavigation({
1717
1983
  items: hours,
@@ -1729,13 +1995,14 @@ function TimePickerHourList({ classNames, ...props }) {
1729
1995
  ...props,
1730
1996
  children: hours.map((hour) => {
1731
1997
  const isSelected = hour === selectedHourDisplay;
1998
+ const isHourFullyDisabled = isHourDisabled(hour);
1732
1999
  const optionClass = [classNames?.option, isSelected && classNames?.optionSelected].filter(Boolean).join(" ") || void 0;
1733
2000
  return /* @__PURE__ */ jsx(
1734
2001
  "li",
1735
2002
  {
1736
2003
  role: "option",
1737
2004
  "aria-selected": isSelected,
1738
- "aria-disabled": isDisabled || void 0,
2005
+ "aria-disabled": isDisabled || isHourFullyDisabled || void 0,
1739
2006
  "aria-label": ctx.labels.hourOption(hour),
1740
2007
  "data-selected": isSelected || void 0,
1741
2008
  tabIndex: isSelected ? 0 : -1,
@@ -1752,14 +2019,22 @@ function TimePickerHourList({ classNames, ...props }) {
1752
2019
  }
1753
2020
  function TimePickerMinuteList({ classNames, ...props }) {
1754
2021
  const ctx = useTimePickerContext("TimePicker.MinuteList");
1755
- const { step, currentTime, isDisabled, isReadOnly } = ctx;
1756
- const minutes = generateMinutes(step);
2022
+ const { step, currentTime, isDisabled, isReadOnly, filterTime } = ctx;
2023
+ const minutes = useMemo(() => generateMinutes(step), [step]);
2024
+ const isMinuteDisabled = useCallback(
2025
+ (minute) => {
2026
+ if (!filterTime) return false;
2027
+ return filterTime(currentTime.hours, minute);
2028
+ },
2029
+ [filterTime, currentTime.hours]
2030
+ );
1757
2031
  const handleSelect = useCallback(
1758
2032
  (minute) => {
1759
2033
  if (isDisabled || isReadOnly) return;
2034
+ if (isMinuteDisabled(minute)) return;
1760
2035
  ctx.setTime({ minutes: minute });
1761
2036
  },
1762
- [ctx, isDisabled, isReadOnly]
2037
+ [ctx, isDisabled, isReadOnly, isMinuteDisabled]
1763
2038
  );
1764
2039
  const { listRef, handleKeyDown } = useListboxNavigation({
1765
2040
  items: minutes,
@@ -1777,13 +2052,14 @@ function TimePickerMinuteList({ classNames, ...props }) {
1777
2052
  ...props,
1778
2053
  children: minutes.map((minute) => {
1779
2054
  const isSelected = minute === currentTime.minutes;
2055
+ const isMinuteFullyDisabled = isMinuteDisabled(minute);
1780
2056
  const optionClass = [classNames?.option, isSelected && classNames?.optionSelected].filter(Boolean).join(" ") || void 0;
1781
2057
  return /* @__PURE__ */ jsx(
1782
2058
  "li",
1783
2059
  {
1784
2060
  role: "option",
1785
2061
  "aria-selected": isSelected,
1786
- "aria-disabled": isDisabled || void 0,
2062
+ "aria-disabled": isDisabled || isMinuteFullyDisabled || void 0,
1787
2063
  "aria-label": ctx.labels.minuteOption(minute),
1788
2064
  "data-selected": isSelected || void 0,
1789
2065
  tabIndex: isSelected ? 0 : -1,
@@ -1800,29 +2076,70 @@ function TimePickerMinuteList({ classNames, ...props }) {
1800
2076
  }
1801
2077
  function TimePickerAmPmToggle({ classNames, ...props }) {
1802
2078
  const ctx = useTimePickerContext("TimePicker.AmPmToggle");
1803
- if (ctx.format !== "12h") return null;
1804
- const { period, hours12 } = to12Hour(ctx.currentTime.hours);
2079
+ const amRef = useRef(null);
2080
+ const pmRef = useRef(null);
1805
2081
  const setPeriod = useCallback(
1806
2082
  (newPeriod) => {
1807
2083
  if (ctx.isDisabled || ctx.isReadOnly) return;
2084
+ const { hours12 } = to12Hour(ctx.currentTime.hours);
1808
2085
  const newHours24 = to24Hour(hours12, newPeriod);
1809
2086
  ctx.setTime({ hours: newHours24 });
1810
2087
  },
1811
- [hours12, ctx]
2088
+ [ctx]
1812
2089
  );
2090
+ if (ctx.format !== "12h") return null;
2091
+ const { period } = to12Hour(ctx.currentTime.hours);
2092
+ const focusOther = (target) => {
2093
+ (target === "AM" ? amRef : pmRef).current?.focus();
2094
+ };
2095
+ const handleKeyDown = (e, target) => {
2096
+ switch (e.key) {
2097
+ case "ArrowRight":
2098
+ case "ArrowDown":
2099
+ case "ArrowLeft":
2100
+ case "ArrowUp": {
2101
+ e.preventDefault();
2102
+ const next = target === "AM" ? "PM" : "AM";
2103
+ setPeriod(next);
2104
+ focusOther(next);
2105
+ break;
2106
+ }
2107
+ case "Home": {
2108
+ e.preventDefault();
2109
+ setPeriod("AM");
2110
+ focusOther("AM");
2111
+ break;
2112
+ }
2113
+ case "End": {
2114
+ e.preventDefault();
2115
+ setPeriod("PM");
2116
+ focusOther("PM");
2117
+ break;
2118
+ }
2119
+ case " ":
2120
+ case "Enter": {
2121
+ e.preventDefault();
2122
+ setPeriod(target);
2123
+ break;
2124
+ }
2125
+ }
2126
+ };
1813
2127
  const renderButton = (target) => {
1814
2128
  const isSelected = period === target;
1815
2129
  const optionClass = [classNames?.option, isSelected && classNames?.optionSelected].filter(Boolean).join(" ") || void 0;
1816
2130
  return /* @__PURE__ */ jsx(
1817
2131
  "button",
1818
2132
  {
2133
+ ref: target === "AM" ? amRef : pmRef,
1819
2134
  type: "button",
1820
2135
  role: "radio",
1821
2136
  "aria-checked": isSelected,
2137
+ tabIndex: isSelected ? 0 : -1,
1822
2138
  "data-selected": isSelected || void 0,
1823
2139
  disabled: ctx.isDisabled,
1824
2140
  className: optionClass,
1825
2141
  onClick: () => setPeriod(target),
2142
+ onKeyDown: (e) => handleKeyDown(e, target),
1826
2143
  children: target
1827
2144
  }
1828
2145
  );
@@ -1849,9 +2166,6 @@ var TimePicker = Object.assign(TimePickerRoot, {
1849
2166
  MinuteList: TimePickerMinuteList,
1850
2167
  AmPmToggle: TimePickerAmPmToggle
1851
2168
  });
1852
- function getDefaultIso2() {
1853
- return DateFnsAdapter.today();
1854
- }
1855
2169
  function DateTimePickerRoot({
1856
2170
  value: controlledValue,
1857
2171
  defaultValue,
@@ -1860,6 +2174,8 @@ function DateTimePickerRoot({
1860
2174
  onCalendarNavigate,
1861
2175
  format = "24h",
1862
2176
  step = 1,
2177
+ withSeconds = false,
2178
+ filterTime,
1863
2179
  disabled = false,
1864
2180
  readOnly = false,
1865
2181
  weekStartsOn = 0,
@@ -1887,10 +2203,10 @@ function DateTimePickerRoot({
1887
2203
  const currentValue = isControlled ? controlledValue ?? null : uncontrolledValue;
1888
2204
  const [isOpen, setIsOpen] = useState(false);
1889
2205
  const [viewMonth, setViewMonth] = useState(
1890
- currentValue ?? adapter.today(displayTimezone)
2206
+ () => currentValue ?? adapter.today(displayTimezone)
1891
2207
  );
1892
2208
  const [focusedDate, setFocusedDate] = useState(
1893
- currentValue ?? adapter.today(displayTimezone)
2209
+ () => currentValue ?? adapter.today(displayTimezone)
1894
2210
  );
1895
2211
  useChangeEffect(isOpen, onOpenChange);
1896
2212
  const viewMonthStart = useMemo(() => adapter.startOfMonth(viewMonth), [viewMonth, adapter]);
@@ -1900,11 +2216,10 @@ function DateTimePickerRoot({
1900
2216
  () => Array.isArray(disabled) ? disabled : [],
1901
2217
  [disabled]
1902
2218
  );
1903
- const baseIso = currentValue ?? getDefaultIso2();
1904
- const currentTime = useMemo(
1905
- () => displayTimezone ? getTimeInTimezone(baseIso, displayTimezone) : getTime(baseIso),
1906
- [baseIso, displayTimezone]
1907
- );
2219
+ const currentTime = useMemo(() => {
2220
+ if (!currentValue) return { hours: 0, minutes: 0, seconds: 0 };
2221
+ return displayTimezone ? getTimeInTimezone(currentValue, displayTimezone) : getTime(currentValue);
2222
+ }, [currentValue, displayTimezone]);
1908
2223
  const updateValue = useCallback(
1909
2224
  (next) => {
1910
2225
  if (isDisabled || readOnly) return;
@@ -1930,11 +2245,11 @@ function DateTimePickerRoot({
1930
2245
  );
1931
2246
  const setTime$1 = useCallback(
1932
2247
  (partial) => {
1933
- const base = currentValue ?? getDefaultIso2();
2248
+ const base = currentValue ?? adapter.today(displayTimezone);
1934
2249
  const merged = displayTimezone ? setTimeInTimezone(base, partial, displayTimezone) : setTime(base, partial);
1935
2250
  updateValue(merged);
1936
2251
  },
1937
- [currentValue, updateValue, displayTimezone]
2252
+ [currentValue, updateValue, displayTimezone, adapter]
1938
2253
  );
1939
2254
  const open = useCallback(() => {
1940
2255
  if (isDisabled || readOnly) return;
@@ -2001,25 +2316,28 @@ function DateTimePickerRoot({
2001
2316
  setTime: setTime$1,
2002
2317
  format,
2003
2318
  step,
2004
- withSeconds: false,
2319
+ withSeconds,
2005
2320
  displayTimezone,
2006
2321
  isDisabled,
2007
2322
  isReadOnly: readOnly,
2008
2323
  currentTime,
2009
2324
  pickerId,
2010
- labels: mergedTimeLabels
2325
+ labels: mergedTimeLabels,
2326
+ filterTime
2011
2327
  }),
2012
2328
  [
2013
2329
  currentValue,
2014
2330
  setTime$1,
2015
2331
  format,
2016
2332
  step,
2333
+ withSeconds,
2017
2334
  displayTimezone,
2018
2335
  isDisabled,
2019
2336
  readOnly,
2020
2337
  currentTime,
2021
2338
  pickerId,
2022
- mergedTimeLabels
2339
+ mergedTimeLabels,
2340
+ filterTime
2023
2341
  ]
2024
2342
  );
2025
2343
  return /* @__PURE__ */ jsx(DatePickerContext.Provider, { value: dateContext, children: /* @__PURE__ */ jsx(TimePickerContext.Provider, { value: timeContext, children }) });
@@ -2048,6 +2366,8 @@ var DateTimePickerInput = forwardRef(
2048
2366
  (e) => {
2049
2367
  if (e.key === "Escape") {
2050
2368
  ctx.close();
2369
+ } else if (e.key === "Enter" && ctx.isOpen) {
2370
+ e.preventDefault();
2051
2371
  } else if (e.key === "ArrowDown" && !ctx.isOpen) {
2052
2372
  e.preventDefault();
2053
2373
  ctx.open();
@@ -2083,6 +2403,7 @@ var DateTimePickerInput = forwardRef(
2083
2403
  );
2084
2404
  }
2085
2405
  );
2406
+ DateTimePickerInput.displayName = "DateTimePicker.Input";
2086
2407
 
2087
2408
  // src/components/DateTimePicker/index.ts
2088
2409
  var DateTimePicker = Object.assign(DateTimePickerRoot, {
@@ -2101,7 +2422,7 @@ function MonthPickerRoot(props) {
2101
2422
  }
2102
2423
  function MonthPickerGrid({ classNames, ...props }) {
2103
2424
  const ctx = useDatePickerContext("MonthPicker.Grid");
2104
- const { adapter, viewMonth, locale, value, displayTimezone, labels } = ctx;
2425
+ const { adapter, viewMonth, locale, value, displayTimezone, labels, disabled } = ctx;
2105
2426
  const currentYear = adapter.getYear(viewMonth);
2106
2427
  const [valueYear, valueMonthZeroBased] = useMemo(() => {
2107
2428
  if (!value) return [null, null];
@@ -2112,9 +2433,19 @@ function MonthPickerGrid({ classNames, ...props }) {
2112
2433
  return [null, null];
2113
2434
  }
2114
2435
  }, [value, adapter, displayTimezone]);
2115
- const today = adapter.today(displayTimezone);
2116
- const todayYear = adapter.getYear(today);
2117
- const todayMonth = adapter.getMonth(today);
2436
+ const [today, setToday] = useState(null);
2437
+ useEffect(() => {
2438
+ setToday(adapter.today(displayTimezone));
2439
+ }, [adapter, displayTimezone]);
2440
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
2441
+ const todayMonth = today !== null ? adapter.getMonth(today) : -1;
2442
+ const monthDisabledFlags = useMemo(
2443
+ () => Array.from({ length: 12 }, (_, i) => {
2444
+ const monthStart = new Date(Date.UTC(currentYear, i, 1)).toISOString();
2445
+ return isRangeFullyDisabled(monthStart, adapter.endOfMonth(monthStart), disabled, adapter);
2446
+ }),
2447
+ [currentYear, disabled, adapter]
2448
+ );
2118
2449
  const navigateYear = useCallback(
2119
2450
  (direction) => {
2120
2451
  ctx.setViewMonth(adapter.addYears(viewMonth, direction));
@@ -2123,17 +2454,23 @@ function MonthPickerGrid({ classNames, ...props }) {
2123
2454
  );
2124
2455
  const handleMonthSelect = useCallback(
2125
2456
  (monthIndex) => {
2457
+ if (monthDisabledFlags[monthIndex]) return;
2126
2458
  const target = new Date(Date.UTC(currentYear, monthIndex, 1)).toISOString();
2127
2459
  ctx.selectDate(target);
2128
2460
  },
2129
- [currentYear, ctx]
2130
- );
2131
- const months = Array.from({ length: 12 }, (_, i) => ({
2132
- index: i,
2133
- name: getMonthName(i, locale),
2134
- isSelected: valueYear === currentYear && valueMonthZeroBased === i,
2135
- isCurrent: todayYear === currentYear && todayMonth === i
2136
- }));
2461
+ [currentYear, ctx, monthDisabledFlags]
2462
+ );
2463
+ const naturalIndex = valueYear === currentYear && valueMonthZeroBased !== null ? valueMonthZeroBased : adapter.getMonth(viewMonth);
2464
+ const firstEnabled = monthDisabledFlags.findIndex((d) => !d);
2465
+ const initialIndex = monthDisabledFlags[naturalIndex] ? firstEnabled === -1 ? naturalIndex : firstEnabled : naturalIndex;
2466
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
2467
+ initialIndex,
2468
+ disabledFlags: monthDisabledFlags,
2469
+ onSelect: handleMonthSelect,
2470
+ onPageUp: () => navigateYear(-1),
2471
+ onPageDown: () => navigateYear(1),
2472
+ onEscape: ctx.close
2473
+ });
2137
2474
  return /* @__PURE__ */ jsxs("div", { className: classNames?.root, ...props, children: [
2138
2475
  /* @__PURE__ */ jsxs("div", { className: classNames?.header, children: [
2139
2476
  /* @__PURE__ */ jsx(
@@ -2158,37 +2495,57 @@ function MonthPickerGrid({ classNames, ...props }) {
2158
2495
  }
2159
2496
  )
2160
2497
  ] }),
2161
- /* @__PURE__ */ jsx("div", { role: "grid", "aria-label": `${currentYear} months`, className: classNames?.grid, children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsx(
2498
+ /* @__PURE__ */ jsx(
2162
2499
  "div",
2163
2500
  {
2164
- role: "row",
2165
- className: classNames?.gridRow,
2166
- style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2167
- children: months.slice(rowIndex * 3, rowIndex * 3 + 3).map((m) => {
2168
- const monthClass = [
2169
- classNames?.month,
2170
- m.isSelected && classNames?.monthSelected,
2171
- m.isCurrent && classNames?.monthCurrent
2172
- ].filter(Boolean).join(" ") || void 0;
2173
- return /* @__PURE__ */ jsx(
2174
- "button",
2175
- {
2176
- type: "button",
2177
- role: "gridcell",
2178
- "aria-selected": m.isSelected || void 0,
2179
- "aria-current": m.isCurrent ? "date" : void 0,
2180
- "data-selected": m.isSelected || void 0,
2181
- "data-current": m.isCurrent || void 0,
2182
- className: monthClass,
2183
- onClick: () => handleMonthSelect(m.index),
2184
- children: m.name
2185
- },
2186
- m.index
2187
- );
2188
- })
2189
- },
2190
- rowIndex
2191
- )) })
2501
+ ref: gridRef,
2502
+ role: "grid",
2503
+ "aria-label": `${currentYear} months`,
2504
+ className: classNames?.grid,
2505
+ onKeyDown: handleKeyDown,
2506
+ children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsx(
2507
+ "div",
2508
+ {
2509
+ role: "row",
2510
+ className: classNames?.gridRow,
2511
+ style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2512
+ children: Array.from({ length: 3 }, (_2, col) => {
2513
+ const i = rowIndex * 3 + col;
2514
+ const isSelected = valueYear === currentYear && valueMonthZeroBased === i;
2515
+ const isCurrent = todayYear === currentYear && todayMonth === i;
2516
+ const isFocused = i === focusedIndex;
2517
+ const isDisabled = monthDisabledFlags[i] ?? false;
2518
+ const cls = [
2519
+ classNames?.month,
2520
+ isSelected && classNames?.monthSelected,
2521
+ isCurrent && classNames?.monthCurrent,
2522
+ isDisabled && classNames?.monthDisabled
2523
+ ].filter(Boolean).join(" ") || void 0;
2524
+ return /* @__PURE__ */ jsx(
2525
+ "button",
2526
+ {
2527
+ type: "button",
2528
+ role: "gridcell",
2529
+ tabIndex: isFocused ? 0 : -1,
2530
+ disabled: isDisabled,
2531
+ "aria-selected": isSelected || void 0,
2532
+ "aria-disabled": isDisabled || void 0,
2533
+ "aria-current": isCurrent ? "date" : void 0,
2534
+ "data-selected": isSelected || void 0,
2535
+ "data-current": isCurrent || void 0,
2536
+ "data-focused": isFocused || void 0,
2537
+ className: cls,
2538
+ onClick: () => handleMonthSelect(i),
2539
+ children: getMonthName(i, locale)
2540
+ },
2541
+ i
2542
+ );
2543
+ })
2544
+ },
2545
+ rowIndex
2546
+ ))
2547
+ }
2548
+ )
2192
2549
  ] });
2193
2550
  }
2194
2551
 
@@ -2205,7 +2562,7 @@ function YearPickerRoot(props) {
2205
2562
  }
2206
2563
  function YearPickerGrid({ classNames, ...props }) {
2207
2564
  const ctx = useDatePickerContext("YearPicker.Grid");
2208
- const { adapter, viewMonth, value, displayTimezone, labels } = ctx;
2565
+ const { adapter, viewMonth, value, displayTimezone, labels, disabled } = ctx;
2209
2566
  const currentYear = adapter.getYear(viewMonth);
2210
2567
  const decadeStart = currentYear - currentYear % 12;
2211
2568
  const valueYear = useMemo(() => {
@@ -2216,7 +2573,20 @@ function YearPickerGrid({ classNames, ...props }) {
2216
2573
  return null;
2217
2574
  }
2218
2575
  }, [value, adapter, displayTimezone]);
2219
- const todayYear = adapter.getYear(adapter.today(displayTimezone));
2576
+ const [today, setToday] = useState(null);
2577
+ useEffect(() => {
2578
+ setToday(adapter.today(displayTimezone));
2579
+ }, [adapter, displayTimezone]);
2580
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
2581
+ const yearDisabledFlags = useMemo(
2582
+ () => Array.from({ length: 12 }, (_, i) => {
2583
+ const year = decadeStart + i;
2584
+ const yearStart = new Date(Date.UTC(year, 0, 1)).toISOString();
2585
+ const yearEnd = new Date(Date.UTC(year, 11, 31, 23, 59, 59, 999)).toISOString();
2586
+ return isRangeFullyDisabled(yearStart, yearEnd, disabled, adapter);
2587
+ }),
2588
+ [decadeStart, disabled, adapter]
2589
+ );
2220
2590
  const navigateDecade = useCallback(
2221
2591
  (direction) => {
2222
2592
  ctx.setViewMonth(adapter.addYears(viewMonth, direction * 12));
@@ -2224,19 +2594,24 @@ function YearPickerGrid({ classNames, ...props }) {
2224
2594
  [adapter, viewMonth, ctx]
2225
2595
  );
2226
2596
  const handleYearSelect = useCallback(
2227
- (year) => {
2597
+ (indexInDecade) => {
2598
+ if (yearDisabledFlags[indexInDecade]) return;
2599
+ const year = decadeStart + indexInDecade;
2228
2600
  const target = new Date(Date.UTC(year, 0, 1)).toISOString();
2229
2601
  ctx.selectDate(target);
2230
2602
  },
2231
- [ctx]
2232
- );
2233
- const years = Array.from({ length: 12 }, (_, i) => {
2234
- const year = decadeStart + i;
2235
- return {
2236
- value: year,
2237
- isSelected: year === valueYear,
2238
- isCurrent: year === todayYear
2239
- };
2603
+ [ctx, decadeStart, yearDisabledFlags]
2604
+ );
2605
+ const naturalIndex = valueYear !== null && valueYear >= decadeStart && valueYear <= decadeStart + 11 ? valueYear - decadeStart : currentYear - decadeStart;
2606
+ const firstEnabled = yearDisabledFlags.findIndex((d) => !d);
2607
+ const initialIndex = yearDisabledFlags[naturalIndex] ? firstEnabled === -1 ? naturalIndex : firstEnabled : naturalIndex;
2608
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
2609
+ initialIndex,
2610
+ disabledFlags: yearDisabledFlags,
2611
+ onSelect: handleYearSelect,
2612
+ onPageUp: () => navigateDecade(-1),
2613
+ onPageDown: () => navigateDecade(1),
2614
+ onEscape: ctx.close
2240
2615
  });
2241
2616
  const rangeLabel = `${decadeStart}\u2013${decadeStart + 11}`;
2242
2617
  return /* @__PURE__ */ jsxs("div", { className: classNames?.root, ...props, children: [
@@ -2263,37 +2638,58 @@ function YearPickerGrid({ classNames, ...props }) {
2263
2638
  }
2264
2639
  )
2265
2640
  ] }),
2266
- /* @__PURE__ */ jsx("div", { role: "grid", "aria-label": rangeLabel, className: classNames?.grid, children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsx(
2641
+ /* @__PURE__ */ jsx(
2267
2642
  "div",
2268
2643
  {
2269
- role: "row",
2270
- className: classNames?.gridRow,
2271
- style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2272
- children: years.slice(rowIndex * 3, rowIndex * 3 + 3).map((y) => {
2273
- const yearClass = [
2274
- classNames?.year,
2275
- y.isSelected && classNames?.yearSelected,
2276
- y.isCurrent && classNames?.yearCurrent
2277
- ].filter(Boolean).join(" ") || void 0;
2278
- return /* @__PURE__ */ jsx(
2279
- "button",
2280
- {
2281
- type: "button",
2282
- role: "gridcell",
2283
- "aria-selected": y.isSelected || void 0,
2284
- "aria-current": y.isCurrent ? "date" : void 0,
2285
- "data-selected": y.isSelected || void 0,
2286
- "data-current": y.isCurrent || void 0,
2287
- className: yearClass,
2288
- onClick: () => handleYearSelect(y.value),
2289
- children: y.value
2290
- },
2291
- y.value
2292
- );
2293
- })
2294
- },
2295
- rowIndex
2296
- )) })
2644
+ ref: gridRef,
2645
+ role: "grid",
2646
+ "aria-label": rangeLabel,
2647
+ className: classNames?.grid,
2648
+ onKeyDown: handleKeyDown,
2649
+ children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsx(
2650
+ "div",
2651
+ {
2652
+ role: "row",
2653
+ className: classNames?.gridRow,
2654
+ style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2655
+ children: Array.from({ length: 3 }, (_2, col) => {
2656
+ const i = rowIndex * 3 + col;
2657
+ const year = decadeStart + i;
2658
+ const isSelected = year === valueYear;
2659
+ const isCurrent = year === todayYear;
2660
+ const isFocused = i === focusedIndex;
2661
+ const isDisabled = yearDisabledFlags[i] ?? false;
2662
+ const cls = [
2663
+ classNames?.year,
2664
+ isSelected && classNames?.yearSelected,
2665
+ isCurrent && classNames?.yearCurrent,
2666
+ isDisabled && classNames?.yearDisabled
2667
+ ].filter(Boolean).join(" ") || void 0;
2668
+ return /* @__PURE__ */ jsx(
2669
+ "button",
2670
+ {
2671
+ type: "button",
2672
+ role: "gridcell",
2673
+ tabIndex: isFocused ? 0 : -1,
2674
+ disabled: isDisabled,
2675
+ "aria-selected": isSelected || void 0,
2676
+ "aria-disabled": isDisabled || void 0,
2677
+ "aria-current": isCurrent ? "date" : void 0,
2678
+ "data-selected": isSelected || void 0,
2679
+ "data-current": isCurrent || void 0,
2680
+ "data-focused": isFocused || void 0,
2681
+ className: cls,
2682
+ onClick: () => handleYearSelect(i),
2683
+ children: year
2684
+ },
2685
+ i
2686
+ );
2687
+ })
2688
+ },
2689
+ rowIndex
2690
+ ))
2691
+ }
2692
+ )
2297
2693
  ] });
2298
2694
  }
2299
2695
 
@@ -2514,7 +2910,7 @@ function useRangePicker(options = {}) {
2514
2910
  adapter
2515
2911
  };
2516
2912
  }
2517
- function getDefaultIso3() {
2913
+ function getDefaultIso() {
2518
2914
  return DateFnsAdapter.today();
2519
2915
  }
2520
2916
  function useTimePicker(options = {}) {
@@ -2532,7 +2928,7 @@ function useTimePicker(options = {}) {
2532
2928
  defaultValue ?? null
2533
2929
  );
2534
2930
  const currentValue = isControlled ? controlledValue ?? null : uncontrolledValue;
2535
- const baseIso = currentValue ?? getDefaultIso3();
2931
+ const baseIso = currentValue ?? getDefaultIso();
2536
2932
  const currentTime = useMemo(
2537
2933
  () => displayTimezone ? getTimeInTimezone(baseIso, displayTimezone) : getTime(baseIso),
2538
2934
  [baseIso, displayTimezone]