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

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, useEffect, useCallback, useContext, useId, useMemo } 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,15 @@ 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);
159
+ useEffect(() => {
160
+ if (isComposingRef.current) return;
161
+ setInputText(null);
162
+ }, [ctx.value]);
157
163
  let formattedValue = "";
158
164
  if (ctx.value) {
159
165
  try {
@@ -170,47 +176,62 @@ var DatePickerInput = forwardRef(
170
176
  },
171
177
  [ctx, onClick]
172
178
  );
179
+ const commitText = useCallback(
180
+ (text) => {
181
+ if (!text) {
182
+ ctx.selectDate(null);
183
+ setInputText(null);
184
+ return true;
185
+ }
186
+ const parsed = parseInputValue(text, ctx.adapter);
187
+ if (parsed) {
188
+ ctx.selectDate(parsed);
189
+ setInputText(null);
190
+ return true;
191
+ }
192
+ return false;
193
+ },
194
+ [ctx]
195
+ );
173
196
  const handleBlur = useCallback(
174
197
  (e) => {
175
198
  if (inputText !== null) {
176
- const parsed = parseInputValue(inputText, ctx.adapter);
177
- if (parsed) {
178
- ctx.selectDate(parsed);
179
- }
199
+ commitText(inputText);
180
200
  setInputText(null);
181
201
  }
182
202
  onBlur?.(e);
183
203
  },
184
- [inputText, displayFormat, ctx, onBlur]
204
+ [inputText, commitText, onBlur]
185
205
  );
186
206
  const handleChange = useCallback(
187
207
  (e) => {
188
208
  const text = e.target.value;
189
209
  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
- }
210
+ if (isComposingRef.current) return;
211
+ commitText(text);
200
212
  },
201
- [displayFormat, ctx]
213
+ [commitText]
214
+ );
215
+ const handleCompositionStart = useCallback(() => {
216
+ isComposingRef.current = true;
217
+ }, []);
218
+ const handleCompositionEnd = useCallback(
219
+ (e) => {
220
+ isComposingRef.current = false;
221
+ commitText(e.target.value);
222
+ },
223
+ [commitText]
202
224
  );
203
225
  const handleKeyDown = useCallback(
204
226
  (e) => {
205
227
  if (e.key === "Escape") {
206
228
  ctx.close();
207
229
  } else if (e.key === "Enter") {
230
+ if (ctx.isOpen) e.preventDefault();
208
231
  if (inputText !== null) {
209
- const parsed = parseInputValue(inputText, ctx.adapter);
210
- if (parsed) {
211
- ctx.selectDate(parsed);
212
- setInputText(null);
213
- }
232
+ commitText(inputText);
233
+ } else if (ctx.isOpen) {
234
+ ctx.selectDate(ctx.focusedDate);
214
235
  }
215
236
  } else if (e.key === "ArrowDown" && !ctx.isOpen) {
216
237
  e.preventDefault();
@@ -218,36 +239,42 @@ var DatePickerInput = forwardRef(
218
239
  }
219
240
  onKeyDown?.(e);
220
241
  },
221
- [ctx, inputText, displayFormat, onKeyDown]
242
+ [ctx, inputText, commitText, onKeyDown]
222
243
  );
223
244
  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
- );
245
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
246
+ /* @__PURE__ */ jsx(
247
+ "input",
248
+ {
249
+ ref: (node) => {
250
+ ctx.referenceRef.current = node;
251
+ if (typeof ref === "function") ref(node);
252
+ else if (ref) ref.current = node;
253
+ },
254
+ type: "text",
255
+ role: "combobox",
256
+ "aria-expanded": ctx.isOpen,
257
+ "aria-haspopup": "dialog",
258
+ "aria-controls": ctx.isOpen ? calendarId : void 0,
259
+ "aria-autocomplete": "none",
260
+ autoComplete: "off",
261
+ value: displayValue,
262
+ disabled: ctx.isDisabled || props.disabled,
263
+ readOnly: ctx.isReadOnly,
264
+ onChange: handleChange,
265
+ onClick: handleClick,
266
+ onBlur: handleBlur,
267
+ onKeyDown: handleKeyDown,
268
+ onCompositionStart: handleCompositionStart,
269
+ onCompositionEnd: handleCompositionEnd,
270
+ ...props
271
+ }
272
+ ),
273
+ name ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value: ctx.value ?? "" }) : null
274
+ ] });
249
275
  }
250
276
  );
277
+ DatePickerInput.displayName = "DatePicker.Input";
251
278
  var DatePickerTrigger = forwardRef(
252
279
  function DatePickerTrigger2({ onClick, children, ...props }, ref) {
253
280
  const ctx = useDatePickerContext("DatePicker.Trigger");
@@ -271,6 +298,7 @@ var DatePickerTrigger = forwardRef(
271
298
  tabIndex: 0,
272
299
  "aria-label": ctx.isOpen ? ctx.labels.triggerClose : ctx.labels.triggerOpen,
273
300
  "aria-expanded": ctx.isOpen,
301
+ "aria-haspopup": "dialog",
274
302
  "aria-controls": ctx.isOpen ? calendarId : void 0,
275
303
  disabled: ctx.isDisabled || props.disabled,
276
304
  onClick: handleClick,
@@ -301,6 +329,8 @@ var DatePickerTrigger = forwardRef(
301
329
  );
302
330
  }
303
331
  );
332
+ DatePickerTrigger.displayName = "DatePicker.Trigger";
333
+ var POPOVER_MIDDLEWARE = [offset(4), flip(), shift({ padding: 8 })];
304
334
  function usePopover({
305
335
  isOpen,
306
336
  close,
@@ -312,7 +342,7 @@ function usePopover({
312
342
  const { refs, floatingStyles, isPositioned } = useFloating({
313
343
  open: isOpen,
314
344
  placement,
315
- middleware: [offset(4), flip(), shift({ padding: 8 })],
345
+ middleware: POPOVER_MIDDLEWARE,
316
346
  whileElementsMounted: autoUpdate
317
347
  });
318
348
  useEffect(() => {
@@ -359,6 +389,24 @@ function usePopover({
359
389
  document.addEventListener("keydown", handleKeyDown);
360
390
  return () => document.removeEventListener("keydown", handleKeyDown);
361
391
  }, [isOpen, close]);
392
+ useEffect(() => {
393
+ if (!isOpen) return;
394
+ function handleFocusOut(e) {
395
+ const next = e.relatedTarget;
396
+ const floating = floatingRef.current;
397
+ const reference = referenceRef.current;
398
+ if (!next) return;
399
+ const insideFloating = floating?.contains(next) ?? false;
400
+ const insideReference = reference?.contains(next) ?? false;
401
+ if (!insideFloating && !insideReference) {
402
+ close();
403
+ }
404
+ }
405
+ const node = floatingRef.current;
406
+ if (!node) return;
407
+ node.addEventListener("focusout", handleFocusOut);
408
+ return () => node.removeEventListener("focusout", handleFocusOut);
409
+ }, [isOpen, close, referenceRef]);
362
410
  const setFloatingRef = useCallback(
363
411
  (node) => {
364
412
  floatingRef.current = node;
@@ -420,20 +468,36 @@ var srOnly = {
420
468
  function DatePickerCalendar({
421
469
  classNames,
422
470
  onTitleClick,
471
+ showWeekNumber = false,
472
+ fixedWeeks = false,
423
473
  ...props
424
474
  }) {
425
475
  const ctx = useDatePickerContext("DatePicker.Calendar");
426
476
  const gridRef = useRef(null);
427
477
  const [announcement, setAnnouncement] = useState("");
428
478
  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
- });
479
+ const weekdays = useMemo(() => getWeekdayNames(locale, weekStartsOn), [locale, weekStartsOn]);
480
+ const weeks = useMemo(
481
+ () => getCalendarDays(viewMonth, adapter, {
482
+ weekStartsOn,
483
+ selected: ctx.value,
484
+ focusedDate,
485
+ disabled,
486
+ timezone: displayTimezone,
487
+ fixedWeeks
488
+ }),
489
+ [
490
+ viewMonth,
491
+ adapter,
492
+ weekStartsOn,
493
+ ctx.value,
494
+ focusedDate,
495
+ disabled,
496
+ displayTimezone,
497
+ fixedWeeks
498
+ ]
499
+ );
500
+ const thursdayIndex = weekStartsOn === 0 ? 4 : 3;
437
501
  const year = adapter.getYear(viewMonth);
438
502
  const month = adapter.getMonth(viewMonth);
439
503
  const title = formatMonthYear(year, month, locale);
@@ -513,6 +577,15 @@ function DatePickerCalendar({
513
577
  }
514
578
  if (newFocused) {
515
579
  e.preventDefault();
580
+ const skipStep = e.key === "ArrowLeft" || e.key === "ArrowUp" || e.key === "PageUp" || e.key === "Home" ? -1 : 1;
581
+ let attempts = 0;
582
+ while (isDateDisabled(newFocused, disabled, adapter) && attempts < 42) {
583
+ newFocused = adapter.addDays(newFocused, skipStep);
584
+ attempts++;
585
+ }
586
+ if (attempts >= 42) {
587
+ return;
588
+ }
516
589
  ctx.setFocusedDate(newFocused);
517
590
  if (!adapter.isSameMonth(newFocused, viewMonth)) {
518
591
  ctx.setViewMonth(newFocused);
@@ -560,62 +633,170 @@ function DatePickerCalendar({
560
633
  ref: gridRef,
561
634
  role: "grid",
562
635
  "aria-label": title,
636
+ "aria-rowcount": weeks.length + 1,
637
+ "aria-colcount": 7,
563
638
  className: classNames?.grid,
564
639
  onKeyDown: handleKeyDown,
565
640
  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",
641
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { role: "row", "aria-rowindex": 1, children: [
642
+ showWeekNumber ? /* @__PURE__ */ jsx("th", { scope: "col", "aria-hidden": "true", className: classNames?.weekNumberHeader, children: "#" }) : null,
643
+ weekdays.map((day, colIndex) => /* @__PURE__ */ jsx(
644
+ "th",
587
645
  {
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",
646
+ role: "columnheader",
647
+ abbr: day.full,
648
+ scope: "col",
649
+ "aria-colindex": colIndex + 1,
650
+ className: classNames?.weekdayHeader,
651
+ children: day.short
652
+ },
653
+ day.short
654
+ ))
655
+ ] }) }),
656
+ /* @__PURE__ */ jsx("tbody", { children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsxs(
657
+ "tr",
658
+ {
659
+ role: "row",
660
+ "aria-rowindex": weekIndex + 2,
661
+ className: classNames?.gridRow,
662
+ children: [
663
+ showWeekNumber ? /* @__PURE__ */ jsx(
664
+ "th",
595
665
  {
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
666
+ scope: "row",
667
+ "aria-hidden": "true",
668
+ className: classNames?.weekNumber,
669
+ "data-week-number": true,
670
+ children: getISOWeekNumber(week[thursdayIndex].isoString)
607
671
  }
608
- )
609
- },
610
- day.isoString
611
- );
612
- }) }, weekIndex)) })
672
+ ) : null,
673
+ week.map((day, colIndex) => {
674
+ const dayClasses = [
675
+ classNames?.day,
676
+ day.isSelected && classNames?.daySelected,
677
+ day.isToday && classNames?.dayToday,
678
+ day.isDisabled && classNames?.dayDisabled,
679
+ !day.isCurrentMonth && classNames?.dayOutsideMonth
680
+ ].filter(Boolean).join(" ") || void 0;
681
+ return /* @__PURE__ */ jsx(
682
+ "td",
683
+ {
684
+ role: "gridcell",
685
+ "aria-colindex": colIndex + 1,
686
+ "aria-selected": day.isSelected || void 0,
687
+ "aria-disabled": day.isDisabled || void 0,
688
+ "aria-current": day.isToday ? "date" : void 0,
689
+ className: classNames?.gridCell,
690
+ children: /* @__PURE__ */ jsx(
691
+ "button",
692
+ {
693
+ type: "button",
694
+ tabIndex: day.isFocused ? 0 : -1,
695
+ disabled: day.isDisabled,
696
+ "data-focused": day.isFocused || void 0,
697
+ "data-selected": day.isSelected || void 0,
698
+ "data-today": day.isToday || void 0,
699
+ "data-outside-month": !day.isCurrentMonth || void 0,
700
+ className: dayClasses,
701
+ onClick: () => handleDayClick(day),
702
+ "aria-label": safeFormatFullDate(day.isoString, locale),
703
+ children: day.dayNumber
704
+ }
705
+ )
706
+ },
707
+ day.isoString
708
+ );
709
+ })
710
+ ]
711
+ },
712
+ weekIndex
713
+ )) })
613
714
  ]
614
715
  }
615
716
  ),
616
717
  /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", style: srOnly, children: announcement })
617
718
  ] });
618
719
  }
720
+ function isRangeFullyDisabled(start, end, rules, adapter) {
721
+ for (const rule of rules) {
722
+ if ("before" in rule && adapter.isBefore(end, rule.before)) return true;
723
+ if ("after" in rule && adapter.isAfter(start, rule.after)) return true;
724
+ }
725
+ return false;
726
+ }
727
+ function useGridState(opts) {
728
+ const { initialIndex, disabledFlags, onSelect, onPageUp, onPageDown, onEscape } = opts;
729
+ const gridRef = useRef(null);
730
+ const [focusedIndex, setFocusedIndex] = useState(initialIndex);
731
+ const handleKeyDown = (e) => {
732
+ let next = null;
733
+ let step = 1;
734
+ switch (e.key) {
735
+ case "ArrowLeft":
736
+ next = Math.max(0, focusedIndex - 1);
737
+ step = -1;
738
+ break;
739
+ case "ArrowRight":
740
+ next = Math.min(11, focusedIndex + 1);
741
+ break;
742
+ case "ArrowUp":
743
+ next = Math.max(0, focusedIndex - 3);
744
+ step = -1;
745
+ break;
746
+ case "ArrowDown":
747
+ next = Math.min(11, focusedIndex + 3);
748
+ break;
749
+ case "Home":
750
+ next = focusedIndex - focusedIndex % 3;
751
+ step = -1;
752
+ break;
753
+ case "End":
754
+ next = focusedIndex - focusedIndex % 3 + 2;
755
+ break;
756
+ case "PageUp":
757
+ e.preventDefault();
758
+ onPageUp();
759
+ return;
760
+ case "PageDown":
761
+ e.preventDefault();
762
+ onPageDown();
763
+ return;
764
+ case "Enter":
765
+ case " ":
766
+ e.preventDefault();
767
+ onSelect(focusedIndex);
768
+ return;
769
+ case "Escape":
770
+ onEscape();
771
+ return;
772
+ default:
773
+ return;
774
+ }
775
+ if (next === null) return;
776
+ e.preventDefault();
777
+ if (disabledFlags) {
778
+ let attempts = 0;
779
+ while (next >= 0 && next < 12 && disabledFlags[next] && attempts < 12) {
780
+ next += step;
781
+ attempts++;
782
+ }
783
+ if (next < 0 || next >= 12 || disabledFlags[next]) return;
784
+ }
785
+ if (next !== focusedIndex) setFocusedIndex(next);
786
+ };
787
+ useEffect(() => {
788
+ if (!disabledFlags || !disabledFlags[focusedIndex]) return;
789
+ const firstEnabled = disabledFlags.findIndex((d) => !d);
790
+ if (firstEnabled !== -1 && firstEnabled !== focusedIndex) {
791
+ setFocusedIndex(firstEnabled);
792
+ }
793
+ }, [disabledFlags, focusedIndex]);
794
+ useEffect(() => {
795
+ const btn = gridRef.current?.querySelector('[data-focused="true"]');
796
+ btn?.focus({ preventScroll: true });
797
+ }, [focusedIndex]);
798
+ return { gridRef, focusedIndex, handleKeyDown };
799
+ }
619
800
  function DatePickerMonthGrid({
620
801
  classNames,
621
802
  onSelect,
@@ -623,15 +804,18 @@ function DatePickerMonthGrid({
623
804
  ...props
624
805
  }) {
625
806
  const ctx = useDatePickerContext("DatePicker.MonthGrid");
626
- const { adapter, viewMonth, locale } = ctx;
807
+ const { adapter, viewMonth, locale, displayTimezone } = ctx;
627
808
  const currentYear = adapter.getYear(viewMonth);
628
809
  const currentMonth = adapter.getMonth(viewMonth);
629
- const todayMonth = adapter.getMonth(adapter.today());
630
- const todayYear = adapter.getYear(adapter.today());
810
+ const [today, setToday] = useState(null);
811
+ useEffect(() => {
812
+ setToday(adapter.today(displayTimezone));
813
+ }, [adapter, displayTimezone]);
814
+ const todayMonth = today !== null ? adapter.getMonth(today) : -1;
815
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
631
816
  const navigateYear = useCallback(
632
817
  (direction) => {
633
- const newDate = adapter.addYears(viewMonth, direction);
634
- ctx.setViewMonth(newDate);
818
+ ctx.setViewMonth(adapter.addYears(viewMonth, direction));
635
819
  },
636
820
  [adapter, viewMonth, ctx]
637
821
  );
@@ -644,12 +828,13 @@ function DatePickerMonthGrid({
644
828
  },
645
829
  [currentYear, ctx, onSelect]
646
830
  );
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
- }));
831
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
832
+ initialIndex: currentMonth,
833
+ onSelect: handleMonthSelect,
834
+ onPageUp: () => navigateYear(-1),
835
+ onPageDown: () => navigateYear(1),
836
+ onEscape: ctx.close
837
+ });
653
838
  return /* @__PURE__ */ jsxs("div", { className: classNames?.root, ...props, children: [
654
839
  /* @__PURE__ */ jsxs("div", { className: classNames?.header, children: [
655
840
  /* @__PURE__ */ jsx(
@@ -677,30 +862,37 @@ function DatePickerMonthGrid({
677
862
  /* @__PURE__ */ jsx(
678
863
  "div",
679
864
  {
865
+ ref: gridRef,
680
866
  role: "grid",
681
867
  "aria-label": `${currentYear} months`,
682
868
  className: classNames?.grid,
683
869
  style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
684
- children: months.map((m) => {
685
- const monthClass = [
870
+ onKeyDown: handleKeyDown,
871
+ children: Array.from({ length: 12 }, (_, i) => {
872
+ const isSelected = i === currentMonth;
873
+ const isCurrent = i === todayMonth && currentYear === todayYear;
874
+ const isFocused = i === focusedIndex;
875
+ const cls = [
686
876
  classNames?.month,
687
- m.isSelected && classNames?.monthSelected,
688
- m.isCurrent && classNames?.monthCurrent
877
+ isSelected && classNames?.monthSelected,
878
+ isCurrent && classNames?.monthCurrent
689
879
  ].filter(Boolean).join(" ") || void 0;
690
880
  return /* @__PURE__ */ jsx(
691
881
  "button",
692
882
  {
693
883
  type: "button",
694
884
  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
885
+ tabIndex: isFocused ? 0 : -1,
886
+ "aria-selected": isSelected || void 0,
887
+ "aria-current": isCurrent ? "date" : void 0,
888
+ "data-selected": isSelected || void 0,
889
+ "data-current": isCurrent || void 0,
890
+ "data-focused": isFocused || void 0,
891
+ className: cls,
892
+ onClick: () => handleMonthSelect(i),
893
+ children: getMonthName(i, locale)
702
894
  },
703
- m.index
895
+ i
704
896
  );
705
897
  })
706
898
  }
@@ -709,38 +901,38 @@ function DatePickerMonthGrid({
709
901
  }
710
902
  function DatePickerYearGrid({ classNames, onSelect, ...props }) {
711
903
  const ctx = useDatePickerContext("DatePicker.YearGrid");
712
- const { adapter, viewMonth } = ctx;
904
+ const { adapter, viewMonth, displayTimezone } = ctx;
713
905
  const currentYear = adapter.getYear(viewMonth);
714
- const todayYear = adapter.getYear(adapter.today());
906
+ const [today, setToday] = useState(null);
907
+ useEffect(() => {
908
+ setToday(adapter.today(displayTimezone));
909
+ }, [adapter, displayTimezone]);
910
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
715
911
  const decadeStart = currentYear - currentYear % 12;
716
912
  const navigateDecade = useCallback(
717
913
  (direction) => {
718
- const newDate = adapter.addYears(viewMonth, direction * 12);
719
- ctx.setViewMonth(newDate);
914
+ ctx.setViewMonth(adapter.addYears(viewMonth, direction * 12));
720
915
  },
721
916
  [adapter, viewMonth, ctx]
722
917
  );
723
918
  const handleYearSelect = useCallback(
724
- (year) => {
919
+ (indexInDecade) => {
920
+ const year = decadeStart + indexInDecade;
725
921
  const currentMonth = adapter.getMonth(viewMonth);
726
922
  const target = new Date(Date.UTC(year, currentMonth, 1)).toISOString();
727
923
  ctx.setViewMonth(target);
728
924
  ctx.setFocusedDate(target);
729
925
  onSelect?.();
730
926
  },
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
- );
927
+ [adapter, viewMonth, ctx, onSelect, decadeStart]
928
+ );
929
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
930
+ initialIndex: currentYear - decadeStart,
931
+ onSelect: handleYearSelect,
932
+ onPageUp: () => navigateDecade(-1),
933
+ onPageDown: () => navigateDecade(1),
934
+ onEscape: ctx.close
935
+ });
744
936
  const rangeLabel = `${decadeStart}\u2013${decadeStart + 11}`;
745
937
  return /* @__PURE__ */ jsxs("div", { className: classNames?.root, ...props, children: [
746
938
  /* @__PURE__ */ jsxs("div", { className: classNames?.header, children: [
@@ -769,30 +961,38 @@ function DatePickerYearGrid({ classNames, onSelect, ...props }) {
769
961
  /* @__PURE__ */ jsx(
770
962
  "div",
771
963
  {
964
+ ref: gridRef,
772
965
  role: "grid",
773
966
  "aria-label": rangeLabel,
774
967
  className: classNames?.grid,
775
968
  style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
776
- children: years.map((y) => {
777
- const yearClass = [
969
+ onKeyDown: handleKeyDown,
970
+ children: Array.from({ length: 12 }, (_, i) => {
971
+ const year = decadeStart + i;
972
+ const isSelected = year === currentYear;
973
+ const isCurrent = year === todayYear;
974
+ const isFocused = i === focusedIndex;
975
+ const cls = [
778
976
  classNames?.year,
779
- y.isSelected && classNames?.yearSelected,
780
- y.isCurrent && classNames?.yearCurrent
977
+ isSelected && classNames?.yearSelected,
978
+ isCurrent && classNames?.yearCurrent
781
979
  ].filter(Boolean).join(" ") || void 0;
782
980
  return /* @__PURE__ */ jsx(
783
981
  "button",
784
982
  {
785
983
  type: "button",
786
984
  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
985
+ tabIndex: isFocused ? 0 : -1,
986
+ "aria-selected": isSelected || void 0,
987
+ "aria-current": isCurrent ? "date" : void 0,
988
+ "data-selected": isSelected || void 0,
989
+ "data-current": isCurrent || void 0,
990
+ "data-focused": isFocused || void 0,
991
+ className: cls,
992
+ onClick: () => handleYearSelect(i),
993
+ children: year
794
994
  },
795
- y.value
995
+ i
796
996
  );
797
997
  })
798
998
  }
@@ -865,8 +1065,7 @@ function DatePickerPreset({
865
1065
  "button",
866
1066
  {
867
1067
  type: "button",
868
- role: "option",
869
- "aria-selected": isActive,
1068
+ "aria-pressed": isActive,
870
1069
  "data-active": isActive || void 0,
871
1070
  disabled: ctx.isDisabled,
872
1071
  onClick: handleClick,
@@ -903,6 +1102,17 @@ function useRangePickerContext(componentName) {
903
1102
  return context;
904
1103
  }
905
1104
  var EMPTY_RANGE = { start: null, end: null };
1105
+ var SR_ONLY = {
1106
+ position: "absolute",
1107
+ width: 1,
1108
+ height: 1,
1109
+ padding: 0,
1110
+ margin: -1,
1111
+ overflow: "hidden",
1112
+ clip: "rect(0, 0, 0, 0)",
1113
+ whiteSpace: "nowrap",
1114
+ border: 0
1115
+ };
906
1116
  function RangePickerRoot({
907
1117
  value: controlledValue,
908
1118
  defaultValue,
@@ -929,11 +1139,13 @@ function RangePickerRoot({
929
1139
  const [isOpen, setIsOpen] = useState(false);
930
1140
  const [selectingTarget, setSelectingTarget] = useState("start");
931
1141
  const [hoverDate, setHoverDate] = useState(null);
1142
+ const [announcement, setAnnouncement] = useState("");
1143
+ const announce = useCallback((message) => setAnnouncement(message), []);
932
1144
  const [viewMonth, setViewMonth] = useState(
933
- currentValue.start ?? adapter.today(displayTimezone)
1145
+ () => currentValue.start ?? adapter.today(displayTimezone)
934
1146
  );
935
1147
  const [focusedDate, setFocusedDate] = useState(
936
- currentValue.start ?? adapter.today(displayTimezone)
1148
+ () => currentValue.start ?? adapter.today(displayTimezone)
937
1149
  );
938
1150
  useChangeEffect(isOpen, onOpenChange);
939
1151
  const viewMonthStart = useMemo(() => adapter.startOfMonth(viewMonth), [viewMonth, adapter]);
@@ -1031,7 +1243,8 @@ function RangePickerRoot({
1031
1243
  isDisabled,
1032
1244
  isReadOnly: readOnly,
1033
1245
  pickerId,
1034
- labels: mergedLabels
1246
+ labels: mergedLabels,
1247
+ announce
1035
1248
  }),
1036
1249
  [
1037
1250
  currentValue,
@@ -1054,10 +1267,14 @@ function RangePickerRoot({
1054
1267
  isDisabled,
1055
1268
  readOnly,
1056
1269
  pickerId,
1057
- mergedLabels
1270
+ mergedLabels,
1271
+ announce
1058
1272
  ]
1059
1273
  );
1060
- return /* @__PURE__ */ jsx(RangePickerContext.Provider, { value: contextValue, children });
1274
+ return /* @__PURE__ */ jsxs(RangePickerContext.Provider, { value: contextValue, children: [
1275
+ children,
1276
+ /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", style: SR_ONLY, children: announcement })
1277
+ ] });
1061
1278
  }
1062
1279
  var RangePickerInput = forwardRef(
1063
1280
  function RangePickerInput2({ part, format: formatProp, onClick, onKeyDown, ...props }, ref) {
@@ -1083,6 +1300,8 @@ var RangePickerInput = forwardRef(
1083
1300
  (e) => {
1084
1301
  if (e.key === "Escape") {
1085
1302
  ctx.close();
1303
+ } else if (e.key === "Enter" && ctx.isOpen) {
1304
+ e.preventDefault();
1086
1305
  } else if (e.key === "ArrowDown" && !ctx.isOpen) {
1087
1306
  e.preventDefault();
1088
1307
  ctx.open();
@@ -1119,6 +1338,7 @@ var RangePickerInput = forwardRef(
1119
1338
  );
1120
1339
  }
1121
1340
  );
1341
+ RangePickerInput.displayName = "RangePicker.Input";
1122
1342
  function RangePickerPopover({ children, ...props }) {
1123
1343
  const ctx = useRangePickerContext("RangePicker.Popover");
1124
1344
  const calendarId = `${ctx.pickerId}-calendar`;
@@ -1154,25 +1374,15 @@ function safeFormatFullDate2(iso, locale) {
1154
1374
  return iso;
1155
1375
  }
1156
1376
  }
1157
- var srOnly2 = {
1158
- position: "absolute",
1159
- width: "1px",
1160
- height: "1px",
1161
- padding: 0,
1162
- margin: "-1px",
1163
- overflow: "hidden",
1164
- clip: "rect(0, 0, 0, 0)",
1165
- whiteSpace: "nowrap",
1166
- border: 0
1167
- };
1168
1377
  function RangePickerCalendar({
1169
1378
  classNames,
1170
1379
  selectionMode = "range",
1380
+ showWeekNumber = false,
1381
+ fixedWeeks = false,
1171
1382
  ...props
1172
1383
  }) {
1173
1384
  const ctx = useRangePickerContext("RangePicker.Calendar");
1174
1385
  const gridRef = useRef(null);
1175
- const [announcement, setAnnouncement] = useState("");
1176
1386
  const {
1177
1387
  adapter,
1178
1388
  viewMonth,
@@ -1185,15 +1395,30 @@ function RangePickerCalendar({
1185
1395
  displayTimezone
1186
1396
  } = ctx;
1187
1397
  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
- });
1398
+ const weekdays = useMemo(() => getWeekdayNames(locale, weekStartsOn), [locale, weekStartsOn]);
1399
+ const weeks = useMemo(
1400
+ () => getCalendarDays(viewMonth, adapter, {
1401
+ weekStartsOn,
1402
+ focusedDate,
1403
+ disabled,
1404
+ range: value,
1405
+ rangeHover: hoverDate,
1406
+ timezone: displayTimezone,
1407
+ fixedWeeks
1408
+ }),
1409
+ [
1410
+ viewMonth,
1411
+ adapter,
1412
+ weekStartsOn,
1413
+ focusedDate,
1414
+ disabled,
1415
+ value,
1416
+ hoverDate,
1417
+ displayTimezone,
1418
+ fixedWeeks
1419
+ ]
1420
+ );
1421
+ const thursdayIndex = weekStartsOn === 0 ? 4 : 3;
1197
1422
  const year = adapter.getYear(viewMonth);
1198
1423
  const month = adapter.getMonth(viewMonth);
1199
1424
  const title = formatMonthYear(year, month, locale);
@@ -1209,7 +1434,7 @@ function RangePickerCalendar({
1209
1434
  ctx.setFocusedDate(adapter.startOfMonth(newMonth));
1210
1435
  const y = adapter.getYear(newMonth);
1211
1436
  const m = adapter.getMonth(newMonth);
1212
- setAnnouncement(formatMonthYear(y, m, locale));
1437
+ ctx.announce(formatMonthYear(y, m, locale));
1213
1438
  },
1214
1439
  [adapter, viewMonth, ctx, locale]
1215
1440
  );
@@ -1221,15 +1446,27 @@ function RangePickerCalendar({
1221
1446
  const range = { start: weekStart, end: weekEnd };
1222
1447
  ctx.setRange(range);
1223
1448
  ctx.close();
1224
- setAnnouncement(
1225
- `${safeFormatFullDate2(weekStart, locale)} \u2013 ${safeFormatFullDate2(weekEnd, locale)}`
1449
+ ctx.announce(
1450
+ `${ctx.labels.rangeSelected}: ${safeFormatFullDate2(weekStart, locale)} \u2013 ${safeFormatFullDate2(weekEnd, locale)}`
1226
1451
  );
1227
1452
  } else {
1453
+ const wasPickingStart = selectingTarget === "start";
1454
+ const previousStart = value.start;
1228
1455
  ctx.selectDate(iso);
1229
- setAnnouncement(safeFormatFullDate2(iso, locale));
1456
+ const formatted = safeFormatFullDate2(iso, locale);
1457
+ if (wasPickingStart) {
1458
+ ctx.announce(`${formatted}. ${ctx.labels.selectingEnd}`);
1459
+ } else if (previousStart) {
1460
+ const [start, end] = adapter.isBefore(iso, previousStart) ? [iso, previousStart] : [previousStart, iso];
1461
+ ctx.announce(
1462
+ `${ctx.labels.rangeSelected}: ${safeFormatFullDate2(start, locale)} \u2013 ${safeFormatFullDate2(end, locale)}`
1463
+ );
1464
+ } else {
1465
+ ctx.announce(formatted);
1466
+ }
1230
1467
  }
1231
1468
  },
1232
- [selectionMode, adapter, weekStartsOn, ctx, locale]
1469
+ [selectionMode, adapter, weekStartsOn, ctx, locale, selectingTarget, value.start]
1233
1470
  );
1234
1471
  const handleDayClick = useCallback(
1235
1472
  (day) => {
@@ -1293,6 +1530,13 @@ function RangePickerCalendar({
1293
1530
  }
1294
1531
  if (newFocused) {
1295
1532
  e.preventDefault();
1533
+ const skipStep = e.key === "ArrowLeft" || e.key === "ArrowUp" || e.key === "PageUp" || e.key === "Home" ? -1 : 1;
1534
+ let attempts = 0;
1535
+ while (isDateDisabled(newFocused, disabled, adapter) && attempts < 42) {
1536
+ newFocused = adapter.addDays(newFocused, skipStep);
1537
+ attempts++;
1538
+ }
1539
+ if (attempts >= 42) return;
1296
1540
  ctx.setFocusedDate(newFocused);
1297
1541
  if (!adapter.isSameMonth(newFocused, viewMonth)) {
1298
1542
  ctx.setViewMonth(newFocused);
@@ -1345,67 +1589,93 @@ function RangePickerCalendar({
1345
1589
  ref: gridRef,
1346
1590
  role: "grid",
1347
1591
  "aria-label": title,
1348
- "aria-multiselectable": "true",
1592
+ "aria-rowcount": weeks.length + 1,
1593
+ "aria-colcount": 7,
1349
1594
  className: classNames?.grid,
1350
1595
  onKeyDown: handleKeyDown,
1351
1596
  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",
1597
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { role: "row", "aria-rowindex": 1, children: [
1598
+ showWeekNumber ? /* @__PURE__ */ jsx("th", { scope: "col", "aria-hidden": "true", className: classNames?.weekNumberHeader, children: "#" }) : null,
1599
+ weekdays.map((day, colIndex) => /* @__PURE__ */ jsx(
1600
+ "th",
1376
1601
  {
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",
1602
+ role: "columnheader",
1603
+ abbr: day.full,
1604
+ scope: "col",
1605
+ "aria-colindex": colIndex + 1,
1606
+ className: classNames?.weekdayHeader,
1607
+ children: day.short
1608
+ },
1609
+ day.short
1610
+ ))
1611
+ ] }) }),
1612
+ /* @__PURE__ */ jsx("tbody", { children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsxs(
1613
+ "tr",
1614
+ {
1615
+ role: "row",
1616
+ "aria-rowindex": weekIndex + 2,
1617
+ className: classNames?.gridRow,
1618
+ children: [
1619
+ showWeekNumber ? /* @__PURE__ */ jsx(
1620
+ "th",
1384
1621
  {
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
1622
+ scope: "row",
1623
+ "aria-hidden": "true",
1624
+ className: classNames?.weekNumber,
1625
+ "data-week-number": true,
1626
+ children: getISOWeekNumber(week[thursdayIndex].isoString)
1399
1627
  }
1400
- )
1401
- },
1402
- day.isoString
1403
- );
1404
- }) }, weekIndex)) })
1628
+ ) : null,
1629
+ week.map((day, colIndex) => {
1630
+ const dayClasses = [
1631
+ classNames?.day,
1632
+ day.isRangeStart && classNames?.dayRangeStart,
1633
+ day.isRangeEnd && classNames?.dayRangeEnd,
1634
+ day.isInRange && classNames?.dayInRange,
1635
+ day.isToday && classNames?.dayToday,
1636
+ day.isDisabled && classNames?.dayDisabled,
1637
+ !day.isCurrentMonth && classNames?.dayOutsideMonth
1638
+ ].filter(Boolean).join(" ") || void 0;
1639
+ const isSelected = selectionMode === "week" ? day.isRangeStart || day.isRangeEnd || day.isInRange : day.isRangeStart || day.isRangeEnd;
1640
+ return /* @__PURE__ */ jsx(
1641
+ "td",
1642
+ {
1643
+ role: "gridcell",
1644
+ "aria-colindex": colIndex + 1,
1645
+ "aria-selected": isSelected || void 0,
1646
+ "aria-disabled": day.isDisabled || void 0,
1647
+ "aria-current": day.isToday ? "date" : void 0,
1648
+ className: classNames?.gridCell,
1649
+ children: /* @__PURE__ */ jsx(
1650
+ "button",
1651
+ {
1652
+ type: "button",
1653
+ tabIndex: day.isFocused ? 0 : -1,
1654
+ disabled: day.isDisabled,
1655
+ "data-focused": day.isFocused || void 0,
1656
+ "data-range-start": day.isRangeStart || void 0,
1657
+ "data-range-end": day.isRangeEnd || void 0,
1658
+ "data-in-range": day.isInRange || void 0,
1659
+ "data-today": day.isToday || void 0,
1660
+ "data-outside-month": !day.isCurrentMonth || void 0,
1661
+ className: dayClasses,
1662
+ onClick: () => handleDayClick(day),
1663
+ onMouseEnter: () => handleDayMouseEnter(day),
1664
+ "aria-label": safeFormatFullDate2(day.isoString, locale),
1665
+ children: day.dayNumber
1666
+ }
1667
+ )
1668
+ },
1669
+ day.isoString
1670
+ );
1671
+ })
1672
+ ]
1673
+ },
1674
+ weekIndex
1675
+ )) })
1405
1676
  ]
1406
1677
  }
1407
- ),
1408
- /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", style: srOnly2, children: announcement })
1678
+ )
1409
1679
  ] });
1410
1680
  }
1411
1681
  function RangePickerPresets({ classNames, children, ...props }) {
@@ -1463,41 +1733,33 @@ function RangePickerPreset({
1463
1733
  ...props
1464
1734
  }) {
1465
1735
  const ctx = useRangePickerContext("RangePicker.Preset");
1736
+ const resolved = useMemo(() => {
1737
+ if (directRange) return directRange;
1738
+ if (presetKey)
1739
+ return resolvePreset(presetKey, ctx.adapter.today(ctx.displayTimezone), ctx.adapter);
1740
+ return null;
1741
+ }, [directRange, presetKey, ctx.adapter, ctx.displayTimezone]);
1466
1742
  const handleClick = useCallback(
1467
1743
  (e) => {
1468
1744
  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
- }
1745
+ if (!resolved) return;
1477
1746
  ctx.setRange(resolved);
1478
1747
  ctx.close();
1479
1748
  onClick?.(e);
1480
1749
  },
1481
- [ctx, presetKey, directRange, onClick]
1750
+ [ctx, resolved, onClick]
1482
1751
  );
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 {
1752
+ const isActive = useMemo(() => {
1753
+ if (!ctx.value.start || !ctx.value.end || !resolved || !resolved.start || !resolved.end) {
1491
1754
  return false;
1492
1755
  }
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
- })();
1756
+ return ctx.adapter.isSameDay(ctx.value.start, resolved.start) && ctx.adapter.isSameDay(ctx.value.end, resolved.end);
1757
+ }, [ctx.value.start, ctx.value.end, ctx.adapter, resolved]);
1495
1758
  return /* @__PURE__ */ jsx(
1496
1759
  "button",
1497
1760
  {
1498
1761
  type: "button",
1499
- role: "option",
1500
- "aria-selected": isActive,
1762
+ "aria-pressed": isActive,
1501
1763
  "data-active": isActive || void 0,
1502
1764
  disabled: ctx.isDisabled,
1503
1765
  onClick: handleClick,
@@ -1530,9 +1792,6 @@ function useTimePickerContext(componentName) {
1530
1792
  }
1531
1793
  return context;
1532
1794
  }
1533
- function getDefaultIso() {
1534
- return DateFnsAdapter.today();
1535
- }
1536
1795
  function TimePickerRoot({
1537
1796
  value: controlledValue,
1538
1797
  defaultValue,
@@ -1543,6 +1802,7 @@ function TimePickerRoot({
1543
1802
  displayTimezone,
1544
1803
  disabled = false,
1545
1804
  readOnly = false,
1805
+ filterTime,
1546
1806
  labels: labelsProp,
1547
1807
  children
1548
1808
  }) {
@@ -1556,21 +1816,21 @@ function TimePickerRoot({
1556
1816
  defaultValue ?? null
1557
1817
  );
1558
1818
  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
- );
1819
+ const currentTime = useMemo(() => {
1820
+ if (!currentValue) return { hours: 0, minutes: 0, seconds: 0 };
1821
+ return displayTimezone ? getTimeInTimezone(currentValue, displayTimezone) : getTime(currentValue);
1822
+ }, [currentValue, displayTimezone]);
1564
1823
  const setTime$1 = useCallback(
1565
1824
  (partial) => {
1566
1825
  if (disabled || readOnly) return;
1567
- const newIso = displayTimezone ? setTimeInTimezone(baseIso, partial, displayTimezone) : setTime(baseIso, partial);
1826
+ const base = currentValue ?? DateFnsAdapter.today(displayTimezone);
1827
+ const newIso = displayTimezone ? setTimeInTimezone(base, partial, displayTimezone) : setTime(base, partial);
1568
1828
  if (!isControlled) {
1569
1829
  setUncontrolledValue(newIso);
1570
1830
  }
1571
1831
  onChange?.(newIso);
1572
1832
  },
1573
- [disabled, readOnly, baseIso, isControlled, onChange, displayTimezone]
1833
+ [disabled, readOnly, currentValue, displayTimezone, isControlled, onChange]
1574
1834
  );
1575
1835
  const contextValue = useMemo(
1576
1836
  () => ({
@@ -1584,7 +1844,8 @@ function TimePickerRoot({
1584
1844
  isReadOnly: readOnly,
1585
1845
  currentTime,
1586
1846
  pickerId,
1587
- labels: mergedLabels
1847
+ labels: mergedLabels,
1848
+ filterTime
1588
1849
  }),
1589
1850
  [
1590
1851
  currentValue,
@@ -1597,7 +1858,8 @@ function TimePickerRoot({
1597
1858
  readOnly,
1598
1859
  currentTime,
1599
1860
  pickerId,
1600
- mergedLabels
1861
+ mergedLabels,
1862
+ filterTime
1601
1863
  ]
1602
1864
  );
1603
1865
  return /* @__PURE__ */ jsx(TimePickerContext.Provider, { value: contextValue, children });
@@ -1606,6 +1868,9 @@ var TimePickerInput = forwardRef(
1606
1868
  function TimePickerInput2({ onBlur, onKeyDown, ...props }, ref) {
1607
1869
  const ctx = useTimePickerContext("TimePicker.Input");
1608
1870
  const [inputText, setInputText] = useState(null);
1871
+ useEffect(() => {
1872
+ setInputText(null);
1873
+ }, [ctx.value]);
1609
1874
  const displayValue = inputText !== null ? inputText : formatTimeString(ctx.currentTime, ctx.withSeconds);
1610
1875
  const commitInput = useCallback(() => {
1611
1876
  if (inputText === null) return;
@@ -1654,6 +1919,7 @@ var TimePickerInput = forwardRef(
1654
1919
  );
1655
1920
  }
1656
1921
  );
1922
+ TimePickerInput.displayName = "TimePicker.Input";
1657
1923
  function useListboxNavigation({
1658
1924
  items,
1659
1925
  onSelect,
@@ -1701,17 +1967,41 @@ function useListboxNavigation({
1701
1967
  }
1702
1968
  function TimePickerHourList({ classNames, ...props }) {
1703
1969
  const ctx = useTimePickerContext("TimePicker.HourList");
1704
- const { format, currentTime, isDisabled, isReadOnly } = ctx;
1705
- const hours = generateHours(format);
1970
+ const { format, step, currentTime, isDisabled, isReadOnly, filterTime } = ctx;
1971
+ const hours = useMemo(() => generateHours(format), [format]);
1706
1972
  const selectedHourDisplay = format === "12h" ? to12Hour(currentTime.hours).hours12 : currentTime.hours;
1707
1973
  const currentPeriod = format === "12h" ? to12Hour(currentTime.hours).period : null;
1974
+ const fullyDisabledHours24 = useMemo(() => {
1975
+ if (!filterTime) return null;
1976
+ const disabled = /* @__PURE__ */ new Set();
1977
+ for (let h = 0; h < 24; h++) {
1978
+ let allRejected = true;
1979
+ for (let m = 0; m < 60; m += step) {
1980
+ if (!filterTime(h, m)) {
1981
+ allRejected = false;
1982
+ break;
1983
+ }
1984
+ }
1985
+ if (allRejected) disabled.add(h);
1986
+ }
1987
+ return disabled;
1988
+ }, [filterTime, step]);
1989
+ const isHourDisabled = useCallback(
1990
+ (hourDisplay) => {
1991
+ if (!fullyDisabledHours24) return false;
1992
+ const hours24 = format === "12h" && currentPeriod ? to24Hour(hourDisplay, currentPeriod) : hourDisplay;
1993
+ return fullyDisabledHours24.has(hours24);
1994
+ },
1995
+ [fullyDisabledHours24, format, currentPeriod]
1996
+ );
1708
1997
  const handleSelect = useCallback(
1709
1998
  (hourDisplay) => {
1710
1999
  if (isDisabled || isReadOnly) return;
2000
+ if (isHourDisabled(hourDisplay)) return;
1711
2001
  const hours24 = format === "12h" && currentPeriod ? to24Hour(hourDisplay, currentPeriod) : hourDisplay;
1712
2002
  ctx.setTime({ hours: hours24 });
1713
2003
  },
1714
- [format, currentPeriod, ctx, isDisabled, isReadOnly]
2004
+ [format, currentPeriod, ctx, isDisabled, isReadOnly, isHourDisabled]
1715
2005
  );
1716
2006
  const { listRef, handleKeyDown } = useListboxNavigation({
1717
2007
  items: hours,
@@ -1729,13 +2019,14 @@ function TimePickerHourList({ classNames, ...props }) {
1729
2019
  ...props,
1730
2020
  children: hours.map((hour) => {
1731
2021
  const isSelected = hour === selectedHourDisplay;
2022
+ const isHourFullyDisabled = isHourDisabled(hour);
1732
2023
  const optionClass = [classNames?.option, isSelected && classNames?.optionSelected].filter(Boolean).join(" ") || void 0;
1733
2024
  return /* @__PURE__ */ jsx(
1734
2025
  "li",
1735
2026
  {
1736
2027
  role: "option",
1737
2028
  "aria-selected": isSelected,
1738
- "aria-disabled": isDisabled || void 0,
2029
+ "aria-disabled": isDisabled || isHourFullyDisabled || void 0,
1739
2030
  "aria-label": ctx.labels.hourOption(hour),
1740
2031
  "data-selected": isSelected || void 0,
1741
2032
  tabIndex: isSelected ? 0 : -1,
@@ -1752,14 +2043,22 @@ function TimePickerHourList({ classNames, ...props }) {
1752
2043
  }
1753
2044
  function TimePickerMinuteList({ classNames, ...props }) {
1754
2045
  const ctx = useTimePickerContext("TimePicker.MinuteList");
1755
- const { step, currentTime, isDisabled, isReadOnly } = ctx;
1756
- const minutes = generateMinutes(step);
2046
+ const { step, currentTime, isDisabled, isReadOnly, filterTime } = ctx;
2047
+ const minutes = useMemo(() => generateMinutes(step), [step]);
2048
+ const isMinuteDisabled = useCallback(
2049
+ (minute) => {
2050
+ if (!filterTime) return false;
2051
+ return filterTime(currentTime.hours, minute);
2052
+ },
2053
+ [filterTime, currentTime.hours]
2054
+ );
1757
2055
  const handleSelect = useCallback(
1758
2056
  (minute) => {
1759
2057
  if (isDisabled || isReadOnly) return;
2058
+ if (isMinuteDisabled(minute)) return;
1760
2059
  ctx.setTime({ minutes: minute });
1761
2060
  },
1762
- [ctx, isDisabled, isReadOnly]
2061
+ [ctx, isDisabled, isReadOnly, isMinuteDisabled]
1763
2062
  );
1764
2063
  const { listRef, handleKeyDown } = useListboxNavigation({
1765
2064
  items: minutes,
@@ -1777,13 +2076,14 @@ function TimePickerMinuteList({ classNames, ...props }) {
1777
2076
  ...props,
1778
2077
  children: minutes.map((minute) => {
1779
2078
  const isSelected = minute === currentTime.minutes;
2079
+ const isMinuteFullyDisabled = isMinuteDisabled(minute);
1780
2080
  const optionClass = [classNames?.option, isSelected && classNames?.optionSelected].filter(Boolean).join(" ") || void 0;
1781
2081
  return /* @__PURE__ */ jsx(
1782
2082
  "li",
1783
2083
  {
1784
2084
  role: "option",
1785
2085
  "aria-selected": isSelected,
1786
- "aria-disabled": isDisabled || void 0,
2086
+ "aria-disabled": isDisabled || isMinuteFullyDisabled || void 0,
1787
2087
  "aria-label": ctx.labels.minuteOption(minute),
1788
2088
  "data-selected": isSelected || void 0,
1789
2089
  tabIndex: isSelected ? 0 : -1,
@@ -1800,29 +2100,70 @@ function TimePickerMinuteList({ classNames, ...props }) {
1800
2100
  }
1801
2101
  function TimePickerAmPmToggle({ classNames, ...props }) {
1802
2102
  const ctx = useTimePickerContext("TimePicker.AmPmToggle");
1803
- if (ctx.format !== "12h") return null;
1804
- const { period, hours12 } = to12Hour(ctx.currentTime.hours);
2103
+ const amRef = useRef(null);
2104
+ const pmRef = useRef(null);
1805
2105
  const setPeriod = useCallback(
1806
2106
  (newPeriod) => {
1807
2107
  if (ctx.isDisabled || ctx.isReadOnly) return;
2108
+ const { hours12 } = to12Hour(ctx.currentTime.hours);
1808
2109
  const newHours24 = to24Hour(hours12, newPeriod);
1809
2110
  ctx.setTime({ hours: newHours24 });
1810
2111
  },
1811
- [hours12, ctx]
2112
+ [ctx]
1812
2113
  );
2114
+ if (ctx.format !== "12h") return null;
2115
+ const { period } = to12Hour(ctx.currentTime.hours);
2116
+ const focusOther = (target) => {
2117
+ (target === "AM" ? amRef : pmRef).current?.focus();
2118
+ };
2119
+ const handleKeyDown = (e, target) => {
2120
+ switch (e.key) {
2121
+ case "ArrowRight":
2122
+ case "ArrowDown":
2123
+ case "ArrowLeft":
2124
+ case "ArrowUp": {
2125
+ e.preventDefault();
2126
+ const next = target === "AM" ? "PM" : "AM";
2127
+ setPeriod(next);
2128
+ focusOther(next);
2129
+ break;
2130
+ }
2131
+ case "Home": {
2132
+ e.preventDefault();
2133
+ setPeriod("AM");
2134
+ focusOther("AM");
2135
+ break;
2136
+ }
2137
+ case "End": {
2138
+ e.preventDefault();
2139
+ setPeriod("PM");
2140
+ focusOther("PM");
2141
+ break;
2142
+ }
2143
+ case " ":
2144
+ case "Enter": {
2145
+ e.preventDefault();
2146
+ setPeriod(target);
2147
+ break;
2148
+ }
2149
+ }
2150
+ };
1813
2151
  const renderButton = (target) => {
1814
2152
  const isSelected = period === target;
1815
2153
  const optionClass = [classNames?.option, isSelected && classNames?.optionSelected].filter(Boolean).join(" ") || void 0;
1816
2154
  return /* @__PURE__ */ jsx(
1817
2155
  "button",
1818
2156
  {
2157
+ ref: target === "AM" ? amRef : pmRef,
1819
2158
  type: "button",
1820
2159
  role: "radio",
1821
2160
  "aria-checked": isSelected,
2161
+ tabIndex: isSelected ? 0 : -1,
1822
2162
  "data-selected": isSelected || void 0,
1823
2163
  disabled: ctx.isDisabled,
1824
2164
  className: optionClass,
1825
2165
  onClick: () => setPeriod(target),
2166
+ onKeyDown: (e) => handleKeyDown(e, target),
1826
2167
  children: target
1827
2168
  }
1828
2169
  );
@@ -1849,9 +2190,6 @@ var TimePicker = Object.assign(TimePickerRoot, {
1849
2190
  MinuteList: TimePickerMinuteList,
1850
2191
  AmPmToggle: TimePickerAmPmToggle
1851
2192
  });
1852
- function getDefaultIso2() {
1853
- return DateFnsAdapter.today();
1854
- }
1855
2193
  function DateTimePickerRoot({
1856
2194
  value: controlledValue,
1857
2195
  defaultValue,
@@ -1860,6 +2198,8 @@ function DateTimePickerRoot({
1860
2198
  onCalendarNavigate,
1861
2199
  format = "24h",
1862
2200
  step = 1,
2201
+ withSeconds = false,
2202
+ filterTime,
1863
2203
  disabled = false,
1864
2204
  readOnly = false,
1865
2205
  weekStartsOn = 0,
@@ -1887,10 +2227,10 @@ function DateTimePickerRoot({
1887
2227
  const currentValue = isControlled ? controlledValue ?? null : uncontrolledValue;
1888
2228
  const [isOpen, setIsOpen] = useState(false);
1889
2229
  const [viewMonth, setViewMonth] = useState(
1890
- currentValue ?? adapter.today(displayTimezone)
2230
+ () => currentValue ?? adapter.today(displayTimezone)
1891
2231
  );
1892
2232
  const [focusedDate, setFocusedDate] = useState(
1893
- currentValue ?? adapter.today(displayTimezone)
2233
+ () => currentValue ?? adapter.today(displayTimezone)
1894
2234
  );
1895
2235
  useChangeEffect(isOpen, onOpenChange);
1896
2236
  const viewMonthStart = useMemo(() => adapter.startOfMonth(viewMonth), [viewMonth, adapter]);
@@ -1900,11 +2240,10 @@ function DateTimePickerRoot({
1900
2240
  () => Array.isArray(disabled) ? disabled : [],
1901
2241
  [disabled]
1902
2242
  );
1903
- const baseIso = currentValue ?? getDefaultIso2();
1904
- const currentTime = useMemo(
1905
- () => displayTimezone ? getTimeInTimezone(baseIso, displayTimezone) : getTime(baseIso),
1906
- [baseIso, displayTimezone]
1907
- );
2243
+ const currentTime = useMemo(() => {
2244
+ if (!currentValue) return { hours: 0, minutes: 0, seconds: 0 };
2245
+ return displayTimezone ? getTimeInTimezone(currentValue, displayTimezone) : getTime(currentValue);
2246
+ }, [currentValue, displayTimezone]);
1908
2247
  const updateValue = useCallback(
1909
2248
  (next) => {
1910
2249
  if (isDisabled || readOnly) return;
@@ -1930,11 +2269,11 @@ function DateTimePickerRoot({
1930
2269
  );
1931
2270
  const setTime$1 = useCallback(
1932
2271
  (partial) => {
1933
- const base = currentValue ?? getDefaultIso2();
2272
+ const base = currentValue ?? adapter.today(displayTimezone);
1934
2273
  const merged = displayTimezone ? setTimeInTimezone(base, partial, displayTimezone) : setTime(base, partial);
1935
2274
  updateValue(merged);
1936
2275
  },
1937
- [currentValue, updateValue, displayTimezone]
2276
+ [currentValue, updateValue, displayTimezone, adapter]
1938
2277
  );
1939
2278
  const open = useCallback(() => {
1940
2279
  if (isDisabled || readOnly) return;
@@ -2001,25 +2340,28 @@ function DateTimePickerRoot({
2001
2340
  setTime: setTime$1,
2002
2341
  format,
2003
2342
  step,
2004
- withSeconds: false,
2343
+ withSeconds,
2005
2344
  displayTimezone,
2006
2345
  isDisabled,
2007
2346
  isReadOnly: readOnly,
2008
2347
  currentTime,
2009
2348
  pickerId,
2010
- labels: mergedTimeLabels
2349
+ labels: mergedTimeLabels,
2350
+ filterTime
2011
2351
  }),
2012
2352
  [
2013
2353
  currentValue,
2014
2354
  setTime$1,
2015
2355
  format,
2016
2356
  step,
2357
+ withSeconds,
2017
2358
  displayTimezone,
2018
2359
  isDisabled,
2019
2360
  readOnly,
2020
2361
  currentTime,
2021
2362
  pickerId,
2022
- mergedTimeLabels
2363
+ mergedTimeLabels,
2364
+ filterTime
2023
2365
  ]
2024
2366
  );
2025
2367
  return /* @__PURE__ */ jsx(DatePickerContext.Provider, { value: dateContext, children: /* @__PURE__ */ jsx(TimePickerContext.Provider, { value: timeContext, children }) });
@@ -2048,6 +2390,8 @@ var DateTimePickerInput = forwardRef(
2048
2390
  (e) => {
2049
2391
  if (e.key === "Escape") {
2050
2392
  ctx.close();
2393
+ } else if (e.key === "Enter" && ctx.isOpen) {
2394
+ e.preventDefault();
2051
2395
  } else if (e.key === "ArrowDown" && !ctx.isOpen) {
2052
2396
  e.preventDefault();
2053
2397
  ctx.open();
@@ -2083,6 +2427,7 @@ var DateTimePickerInput = forwardRef(
2083
2427
  );
2084
2428
  }
2085
2429
  );
2430
+ DateTimePickerInput.displayName = "DateTimePicker.Input";
2086
2431
 
2087
2432
  // src/components/DateTimePicker/index.ts
2088
2433
  var DateTimePicker = Object.assign(DateTimePickerRoot, {
@@ -2101,7 +2446,7 @@ function MonthPickerRoot(props) {
2101
2446
  }
2102
2447
  function MonthPickerGrid({ classNames, ...props }) {
2103
2448
  const ctx = useDatePickerContext("MonthPicker.Grid");
2104
- const { adapter, viewMonth, locale, value, displayTimezone, labels } = ctx;
2449
+ const { adapter, viewMonth, locale, value, displayTimezone, labels, disabled } = ctx;
2105
2450
  const currentYear = adapter.getYear(viewMonth);
2106
2451
  const [valueYear, valueMonthZeroBased] = useMemo(() => {
2107
2452
  if (!value) return [null, null];
@@ -2112,9 +2457,19 @@ function MonthPickerGrid({ classNames, ...props }) {
2112
2457
  return [null, null];
2113
2458
  }
2114
2459
  }, [value, adapter, displayTimezone]);
2115
- const today = adapter.today(displayTimezone);
2116
- const todayYear = adapter.getYear(today);
2117
- const todayMonth = adapter.getMonth(today);
2460
+ const [today, setToday] = useState(null);
2461
+ useEffect(() => {
2462
+ setToday(adapter.today(displayTimezone));
2463
+ }, [adapter, displayTimezone]);
2464
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
2465
+ const todayMonth = today !== null ? adapter.getMonth(today) : -1;
2466
+ const monthDisabledFlags = useMemo(
2467
+ () => Array.from({ length: 12 }, (_, i) => {
2468
+ const monthStart = new Date(Date.UTC(currentYear, i, 1)).toISOString();
2469
+ return isRangeFullyDisabled(monthStart, adapter.endOfMonth(monthStart), disabled, adapter);
2470
+ }),
2471
+ [currentYear, disabled, adapter]
2472
+ );
2118
2473
  const navigateYear = useCallback(
2119
2474
  (direction) => {
2120
2475
  ctx.setViewMonth(adapter.addYears(viewMonth, direction));
@@ -2123,17 +2478,23 @@ function MonthPickerGrid({ classNames, ...props }) {
2123
2478
  );
2124
2479
  const handleMonthSelect = useCallback(
2125
2480
  (monthIndex) => {
2481
+ if (monthDisabledFlags[monthIndex]) return;
2126
2482
  const target = new Date(Date.UTC(currentYear, monthIndex, 1)).toISOString();
2127
2483
  ctx.selectDate(target);
2128
2484
  },
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
- }));
2485
+ [currentYear, ctx, monthDisabledFlags]
2486
+ );
2487
+ const naturalIndex = valueYear === currentYear && valueMonthZeroBased !== null ? valueMonthZeroBased : adapter.getMonth(viewMonth);
2488
+ const firstEnabled = monthDisabledFlags.findIndex((d) => !d);
2489
+ const initialIndex = monthDisabledFlags[naturalIndex] ? firstEnabled === -1 ? naturalIndex : firstEnabled : naturalIndex;
2490
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
2491
+ initialIndex,
2492
+ disabledFlags: monthDisabledFlags,
2493
+ onSelect: handleMonthSelect,
2494
+ onPageUp: () => navigateYear(-1),
2495
+ onPageDown: () => navigateYear(1),
2496
+ onEscape: ctx.close
2497
+ });
2137
2498
  return /* @__PURE__ */ jsxs("div", { className: classNames?.root, ...props, children: [
2138
2499
  /* @__PURE__ */ jsxs("div", { className: classNames?.header, children: [
2139
2500
  /* @__PURE__ */ jsx(
@@ -2158,37 +2519,57 @@ function MonthPickerGrid({ classNames, ...props }) {
2158
2519
  }
2159
2520
  )
2160
2521
  ] }),
2161
- /* @__PURE__ */ jsx("div", { role: "grid", "aria-label": `${currentYear} months`, className: classNames?.grid, children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsx(
2522
+ /* @__PURE__ */ jsx(
2162
2523
  "div",
2163
2524
  {
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
- )) })
2525
+ ref: gridRef,
2526
+ role: "grid",
2527
+ "aria-label": `${currentYear} months`,
2528
+ className: classNames?.grid,
2529
+ onKeyDown: handleKeyDown,
2530
+ children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsx(
2531
+ "div",
2532
+ {
2533
+ role: "row",
2534
+ className: classNames?.gridRow,
2535
+ style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2536
+ children: Array.from({ length: 3 }, (_2, col) => {
2537
+ const i = rowIndex * 3 + col;
2538
+ const isSelected = valueYear === currentYear && valueMonthZeroBased === i;
2539
+ const isCurrent = todayYear === currentYear && todayMonth === i;
2540
+ const isFocused = i === focusedIndex;
2541
+ const isDisabled = monthDisabledFlags[i] ?? false;
2542
+ const cls = [
2543
+ classNames?.month,
2544
+ isSelected && classNames?.monthSelected,
2545
+ isCurrent && classNames?.monthCurrent,
2546
+ isDisabled && classNames?.monthDisabled
2547
+ ].filter(Boolean).join(" ") || void 0;
2548
+ return /* @__PURE__ */ jsx(
2549
+ "button",
2550
+ {
2551
+ type: "button",
2552
+ role: "gridcell",
2553
+ tabIndex: isFocused ? 0 : -1,
2554
+ disabled: isDisabled,
2555
+ "aria-selected": isSelected || void 0,
2556
+ "aria-disabled": isDisabled || void 0,
2557
+ "aria-current": isCurrent ? "date" : void 0,
2558
+ "data-selected": isSelected || void 0,
2559
+ "data-current": isCurrent || void 0,
2560
+ "data-focused": isFocused || void 0,
2561
+ className: cls,
2562
+ onClick: () => handleMonthSelect(i),
2563
+ children: getMonthName(i, locale)
2564
+ },
2565
+ i
2566
+ );
2567
+ })
2568
+ },
2569
+ rowIndex
2570
+ ))
2571
+ }
2572
+ )
2192
2573
  ] });
2193
2574
  }
2194
2575
 
@@ -2205,7 +2586,7 @@ function YearPickerRoot(props) {
2205
2586
  }
2206
2587
  function YearPickerGrid({ classNames, ...props }) {
2207
2588
  const ctx = useDatePickerContext("YearPicker.Grid");
2208
- const { adapter, viewMonth, value, displayTimezone, labels } = ctx;
2589
+ const { adapter, viewMonth, value, displayTimezone, labels, disabled } = ctx;
2209
2590
  const currentYear = adapter.getYear(viewMonth);
2210
2591
  const decadeStart = currentYear - currentYear % 12;
2211
2592
  const valueYear = useMemo(() => {
@@ -2216,7 +2597,20 @@ function YearPickerGrid({ classNames, ...props }) {
2216
2597
  return null;
2217
2598
  }
2218
2599
  }, [value, adapter, displayTimezone]);
2219
- const todayYear = adapter.getYear(adapter.today(displayTimezone));
2600
+ const [today, setToday] = useState(null);
2601
+ useEffect(() => {
2602
+ setToday(adapter.today(displayTimezone));
2603
+ }, [adapter, displayTimezone]);
2604
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
2605
+ const yearDisabledFlags = useMemo(
2606
+ () => Array.from({ length: 12 }, (_, i) => {
2607
+ const year = decadeStart + i;
2608
+ const yearStart = new Date(Date.UTC(year, 0, 1)).toISOString();
2609
+ const yearEnd = new Date(Date.UTC(year, 11, 31, 23, 59, 59, 999)).toISOString();
2610
+ return isRangeFullyDisabled(yearStart, yearEnd, disabled, adapter);
2611
+ }),
2612
+ [decadeStart, disabled, adapter]
2613
+ );
2220
2614
  const navigateDecade = useCallback(
2221
2615
  (direction) => {
2222
2616
  ctx.setViewMonth(adapter.addYears(viewMonth, direction * 12));
@@ -2224,19 +2618,24 @@ function YearPickerGrid({ classNames, ...props }) {
2224
2618
  [adapter, viewMonth, ctx]
2225
2619
  );
2226
2620
  const handleYearSelect = useCallback(
2227
- (year) => {
2621
+ (indexInDecade) => {
2622
+ if (yearDisabledFlags[indexInDecade]) return;
2623
+ const year = decadeStart + indexInDecade;
2228
2624
  const target = new Date(Date.UTC(year, 0, 1)).toISOString();
2229
2625
  ctx.selectDate(target);
2230
2626
  },
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
- };
2627
+ [ctx, decadeStart, yearDisabledFlags]
2628
+ );
2629
+ const naturalIndex = valueYear !== null && valueYear >= decadeStart && valueYear <= decadeStart + 11 ? valueYear - decadeStart : currentYear - decadeStart;
2630
+ const firstEnabled = yearDisabledFlags.findIndex((d) => !d);
2631
+ const initialIndex = yearDisabledFlags[naturalIndex] ? firstEnabled === -1 ? naturalIndex : firstEnabled : naturalIndex;
2632
+ const { gridRef, focusedIndex, handleKeyDown } = useGridState({
2633
+ initialIndex,
2634
+ disabledFlags: yearDisabledFlags,
2635
+ onSelect: handleYearSelect,
2636
+ onPageUp: () => navigateDecade(-1),
2637
+ onPageDown: () => navigateDecade(1),
2638
+ onEscape: ctx.close
2240
2639
  });
2241
2640
  const rangeLabel = `${decadeStart}\u2013${decadeStart + 11}`;
2242
2641
  return /* @__PURE__ */ jsxs("div", { className: classNames?.root, ...props, children: [
@@ -2263,37 +2662,58 @@ function YearPickerGrid({ classNames, ...props }) {
2263
2662
  }
2264
2663
  )
2265
2664
  ] }),
2266
- /* @__PURE__ */ jsx("div", { role: "grid", "aria-label": rangeLabel, className: classNames?.grid, children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsx(
2665
+ /* @__PURE__ */ jsx(
2267
2666
  "div",
2268
2667
  {
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
- )) })
2668
+ ref: gridRef,
2669
+ role: "grid",
2670
+ "aria-label": rangeLabel,
2671
+ className: classNames?.grid,
2672
+ onKeyDown: handleKeyDown,
2673
+ children: Array.from({ length: 4 }, (_, rowIndex) => /* @__PURE__ */ jsx(
2674
+ "div",
2675
+ {
2676
+ role: "row",
2677
+ className: classNames?.gridRow,
2678
+ style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)" },
2679
+ children: Array.from({ length: 3 }, (_2, col) => {
2680
+ const i = rowIndex * 3 + col;
2681
+ const year = decadeStart + i;
2682
+ const isSelected = year === valueYear;
2683
+ const isCurrent = year === todayYear;
2684
+ const isFocused = i === focusedIndex;
2685
+ const isDisabled = yearDisabledFlags[i] ?? false;
2686
+ const cls = [
2687
+ classNames?.year,
2688
+ isSelected && classNames?.yearSelected,
2689
+ isCurrent && classNames?.yearCurrent,
2690
+ isDisabled && classNames?.yearDisabled
2691
+ ].filter(Boolean).join(" ") || void 0;
2692
+ return /* @__PURE__ */ jsx(
2693
+ "button",
2694
+ {
2695
+ type: "button",
2696
+ role: "gridcell",
2697
+ tabIndex: isFocused ? 0 : -1,
2698
+ disabled: isDisabled,
2699
+ "aria-selected": isSelected || void 0,
2700
+ "aria-disabled": isDisabled || void 0,
2701
+ "aria-current": isCurrent ? "date" : void 0,
2702
+ "data-selected": isSelected || void 0,
2703
+ "data-current": isCurrent || void 0,
2704
+ "data-focused": isFocused || void 0,
2705
+ className: cls,
2706
+ onClick: () => handleYearSelect(i),
2707
+ children: year
2708
+ },
2709
+ i
2710
+ );
2711
+ })
2712
+ },
2713
+ rowIndex
2714
+ ))
2715
+ }
2716
+ )
2297
2717
  ] });
2298
2718
  }
2299
2719
 
@@ -2514,7 +2934,7 @@ function useRangePicker(options = {}) {
2514
2934
  adapter
2515
2935
  };
2516
2936
  }
2517
- function getDefaultIso3() {
2937
+ function getDefaultIso() {
2518
2938
  return DateFnsAdapter.today();
2519
2939
  }
2520
2940
  function useTimePicker(options = {}) {
@@ -2532,7 +2952,7 @@ function useTimePicker(options = {}) {
2532
2952
  defaultValue ?? null
2533
2953
  );
2534
2954
  const currentValue = isControlled ? controlledValue ?? null : uncontrolledValue;
2535
- const baseIso = currentValue ?? getDefaultIso3();
2955
+ const baseIso = currentValue ?? getDefaultIso();
2536
2956
  const currentTime = useMemo(
2537
2957
  () => displayTimezone ? getTimeInTimezone(baseIso, displayTimezone) : getTime(baseIso),
2538
2958
  [baseIso, displayTimezone]