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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @kalyx/react
2
2
 
3
+ ## 1.0.0-rc.4
4
+
5
+ ### Patch Changes
6
+
7
+ - df97687: P1 audit follow-ups for v1.0-rc:
8
+ - **SSR hydration safety in 4 commit/drilldown grids** — `DatePicker.MonthGrid`, `DatePicker.YearGrid`, `MonthPicker.Grid`, and `YearPicker.Grid` previously called `adapter.today()` directly inside their render bodies, producing a server/client clock-mismatch hydration warning across day boundaries (and intermittently wrong "today" highlights in tz-different SSR setups). Today is now snapshotted via `useState(null)` + post-mount `useEffect`, so the server output and the first client render agree, and the highlight settles on the first effect tick.
9
+ - **`AmPmToggle` now follows the WAI-ARIA radiogroup pattern** — Arrow / Home / End / Space / Enter move and commit selection between AM and PM, and `tabIndex` is roving (only the checked radio is in the tab order). Previously both buttons were tabbable and arrow keys were ignored.
10
+ - **`DatePicker.Preset` / `RangePicker.Preset` now use `aria-pressed`** instead of `role="option"` + `aria-selected`. `role="option"` is invalid outside `role="listbox"` / `role="combobox"`, so axe was flagging the previous markup. Active state still appears on `data-active` for CSS targeting.
11
+ - **`RangePicker.Calendar` no longer advertises `aria-multiselectable="true"`** — a date range is one selection (two endpoints), not a multi-select grid.
12
+ - **Test stability** — `useRangePicker` `respects disabled rules` test pinned to April 2026 via `defaultValue` so the calendar grid contains the expected weekend day regardless of the system clock (was failing once the clock crossed into May).
13
+ - **`labels.ts` test coverage** — first unit tests for the default-label exports.
14
+
15
+ Behavioral notes for users (none of these are breaking for code that follows the documented `data-*` styling contract):
16
+ - If you targeted Preset buttons via `[aria-selected="true"]` in CSS, switch to `[aria-pressed="true"]` or `[data-active]`.
17
+ - If you targeted the range grid via `[aria-multiselectable]`, that attribute is gone; use `[role="grid"]` on the calendar root instead.
18
+
19
+ - Updated dependencies [df97687]
20
+ - @kalyx/core@1.0.0-rc.4
21
+
22
+ ## 1.0.0-rc.3
23
+
24
+ ### Patch Changes
25
+
26
+ - 3587b13: Replace deprecated `MutableRefObject<T>` with `RefObject<T>` in context types.
27
+
28
+ `@types/react@19` marks `MutableRefObject` as deprecated (`Use 'RefObject' instead`). In React 19 `RefObject<T>` is itself mutable, so the swap is type-equivalent for the existing `referenceRef` usage in `DatePickerContext` and `RangePickerContext`.
29
+
30
+ No runtime change. No public API surface change.
31
+
32
+ - Updated dependencies [3587b13]
33
+ - @kalyx/core@1.0.0-rc.3
34
+
3
35
  ## 1.0.0-rc.2
4
36
 
5
37
  ### Patch Changes
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @kalyx/react
2
2
 
3
- > The headless React DatePicker, finally complete. Zero CSS · SSR-safe · under 12 KB gzip.
3
+ > The headless React DatePicker, finally complete. Zero CSS · SSR-safe · ~12 KB gzip (≤ 13 KB ceiling).
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@kalyx/react?color=5b4fe1)](https://www.npmjs.com/package/@kalyx/react)
6
- [![Bundle](https://img.shields.io/badge/gzip-11.36KB-brightgreen)](https://kalyx-docs.vercel.app/docs/api/react#bundle-size)
6
+ [![Bundle](https://img.shields.io/badge/gzip-12.27KB-brightgreen)](https://kalyx-docs.vercel.app/docs/api/react#bundle-size)
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](https://www.typescriptlang.org/)
8
8
  [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/jiji-hoon96/kalyx/blob/main/LICENSE)
9
9
 
package/dist/index.cjs CHANGED
@@ -693,11 +693,15 @@ function DatePickerMonthGrid({
693
693
  ...props
694
694
  }) {
695
695
  const ctx = useDatePickerContext("DatePicker.MonthGrid");
696
- const { adapter, viewMonth, locale } = ctx;
696
+ const { adapter, viewMonth, locale, displayTimezone } = ctx;
697
697
  const currentYear = adapter.getYear(viewMonth);
698
698
  const currentMonth = adapter.getMonth(viewMonth);
699
- const todayMonth = adapter.getMonth(adapter.today());
700
- const todayYear = adapter.getYear(adapter.today());
699
+ const [today, setToday] = react.useState(null);
700
+ react.useEffect(() => {
701
+ setToday(adapter.today(displayTimezone));
702
+ }, [adapter, displayTimezone]);
703
+ const todayMonth = today !== null ? adapter.getMonth(today) : -1;
704
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
701
705
  const navigateYear = react.useCallback(
702
706
  (direction) => {
703
707
  const newDate = adapter.addYears(viewMonth, direction);
@@ -779,9 +783,13 @@ function DatePickerMonthGrid({
779
783
  }
780
784
  function DatePickerYearGrid({ classNames, onSelect, ...props }) {
781
785
  const ctx = useDatePickerContext("DatePicker.YearGrid");
782
- const { adapter, viewMonth } = ctx;
786
+ const { adapter, viewMonth, displayTimezone } = ctx;
783
787
  const currentYear = adapter.getYear(viewMonth);
784
- const todayYear = adapter.getYear(adapter.today());
788
+ const [today, setToday] = react.useState(null);
789
+ react.useEffect(() => {
790
+ setToday(adapter.today(displayTimezone));
791
+ }, [adapter, displayTimezone]);
792
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
785
793
  const decadeStart = currentYear - currentYear % 12;
786
794
  const navigateDecade = react.useCallback(
787
795
  (direction) => {
@@ -935,8 +943,7 @@ function DatePickerPreset({
935
943
  "button",
936
944
  {
937
945
  type: "button",
938
- role: "option",
939
- "aria-selected": isActive,
946
+ "aria-pressed": isActive,
940
947
  "data-active": isActive || void 0,
941
948
  disabled: ctx.isDisabled,
942
949
  onClick: handleClick,
@@ -1428,7 +1435,6 @@ function RangePickerCalendar({
1428
1435
  ref: gridRef,
1429
1436
  role: "grid",
1430
1437
  "aria-label": title,
1431
- "aria-multiselectable": "true",
1432
1438
  "aria-rowcount": weeks.length + 1,
1433
1439
  "aria-colcount": 7,
1434
1440
  className: classNames?.grid,
@@ -1592,8 +1598,7 @@ function RangePickerPreset({
1592
1598
  "button",
1593
1599
  {
1594
1600
  type: "button",
1595
- role: "option",
1596
- "aria-selected": isActive,
1601
+ "aria-pressed": isActive,
1597
1602
  "data-active": isActive || void 0,
1598
1603
  disabled: ctx.isDisabled,
1599
1604
  onClick: handleClick,
@@ -1897,29 +1902,70 @@ function TimePickerMinuteList({ classNames, ...props }) {
1897
1902
  }
1898
1903
  function TimePickerAmPmToggle({ classNames, ...props }) {
1899
1904
  const ctx = useTimePickerContext("TimePicker.AmPmToggle");
1900
- if (ctx.format !== "12h") return null;
1901
- const { period, hours12 } = core.to12Hour(ctx.currentTime.hours);
1905
+ const amRef = react.useRef(null);
1906
+ const pmRef = react.useRef(null);
1902
1907
  const setPeriod = react.useCallback(
1903
1908
  (newPeriod) => {
1904
1909
  if (ctx.isDisabled || ctx.isReadOnly) return;
1910
+ const { hours12 } = core.to12Hour(ctx.currentTime.hours);
1905
1911
  const newHours24 = core.to24Hour(hours12, newPeriod);
1906
1912
  ctx.setTime({ hours: newHours24 });
1907
1913
  },
1908
- [hours12, ctx]
1914
+ [ctx]
1909
1915
  );
1916
+ if (ctx.format !== "12h") return null;
1917
+ const { period } = core.to12Hour(ctx.currentTime.hours);
1918
+ const focusOther = (target) => {
1919
+ (target === "AM" ? amRef : pmRef).current?.focus();
1920
+ };
1921
+ const handleKeyDown = (e, target) => {
1922
+ switch (e.key) {
1923
+ case "ArrowRight":
1924
+ case "ArrowDown":
1925
+ case "ArrowLeft":
1926
+ case "ArrowUp": {
1927
+ e.preventDefault();
1928
+ const next = target === "AM" ? "PM" : "AM";
1929
+ setPeriod(next);
1930
+ focusOther(next);
1931
+ break;
1932
+ }
1933
+ case "Home": {
1934
+ e.preventDefault();
1935
+ setPeriod("AM");
1936
+ focusOther("AM");
1937
+ break;
1938
+ }
1939
+ case "End": {
1940
+ e.preventDefault();
1941
+ setPeriod("PM");
1942
+ focusOther("PM");
1943
+ break;
1944
+ }
1945
+ case " ":
1946
+ case "Enter": {
1947
+ e.preventDefault();
1948
+ setPeriod(target);
1949
+ break;
1950
+ }
1951
+ }
1952
+ };
1910
1953
  const renderButton = (target) => {
1911
1954
  const isSelected = period === target;
1912
1955
  const optionClass = [classNames?.option, isSelected && classNames?.optionSelected].filter(Boolean).join(" ") || void 0;
1913
1956
  return /* @__PURE__ */ jsxRuntime.jsx(
1914
1957
  "button",
1915
1958
  {
1959
+ ref: target === "AM" ? amRef : pmRef,
1916
1960
  type: "button",
1917
1961
  role: "radio",
1918
1962
  "aria-checked": isSelected,
1963
+ tabIndex: isSelected ? 0 : -1,
1919
1964
  "data-selected": isSelected || void 0,
1920
1965
  disabled: ctx.isDisabled,
1921
1966
  className: optionClass,
1922
1967
  onClick: () => setPeriod(target),
1968
+ onKeyDown: (e) => handleKeyDown(e, target),
1923
1969
  children: target
1924
1970
  }
1925
1971
  );
@@ -2212,9 +2258,12 @@ function MonthPickerGrid({ classNames, ...props }) {
2212
2258
  return [null, null];
2213
2259
  }
2214
2260
  }, [value, adapter, displayTimezone]);
2215
- const today = adapter.today(displayTimezone);
2216
- const todayYear = adapter.getYear(today);
2217
- const todayMonth = adapter.getMonth(today);
2261
+ const [today, setToday] = react.useState(null);
2262
+ react.useEffect(() => {
2263
+ setToday(adapter.today(displayTimezone));
2264
+ }, [adapter, displayTimezone]);
2265
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
2266
+ const todayMonth = today !== null ? adapter.getMonth(today) : -1;
2218
2267
  const navigateYear = react.useCallback(
2219
2268
  (direction) => {
2220
2269
  ctx.setViewMonth(adapter.addYears(viewMonth, direction));
@@ -2316,7 +2365,11 @@ function YearPickerGrid({ classNames, ...props }) {
2316
2365
  return null;
2317
2366
  }
2318
2367
  }, [value, adapter, displayTimezone]);
2319
- const todayYear = adapter.getYear(adapter.today(displayTimezone));
2368
+ const [today, setToday] = react.useState(null);
2369
+ react.useEffect(() => {
2370
+ setToday(adapter.today(displayTimezone));
2371
+ }, [adapter, displayTimezone]);
2372
+ const todayYear = today !== null ? adapter.getYear(today) : -1;
2320
2373
  const navigateDecade = react.useCallback(
2321
2374
  (direction) => {
2322
2375
  ctx.setViewMonth(adapter.addYears(viewMonth, direction * 12));