@xsolla/xui-calendar 0.226.0 → 0.228.0
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/README.md +33 -16
- package/native/index.d.mts +6 -0
- package/native/index.d.ts +6 -0
- package/native/index.js +125 -11
- package/native/index.js.map +1 -1
- package/native/index.mjs +145 -24
- package/native/index.mjs.map +1 -1
- package/package.json +7 -7
- package/web/index.d.mts +6 -0
- package/web/index.d.ts +6 -0
- package/web/index.js +125 -11
- package/web/index.js.map +1 -1
- package/web/index.mjs +145 -24
- package/web/index.mjs.map +1 -1
package/README.md
CHANGED
|
@@ -208,6 +208,7 @@ export default function DualRange() {
|
|
|
208
208
|
- Each panel navigates on its own. The arrows and the month/year dropdowns of one panel do not move the other panel.
|
|
209
209
|
- A chip, or a `month` value that neither panel shows, re-pairs the panels: the left panel shows that month and the right panel shows the next month.
|
|
210
210
|
- With a complete range, a click in the right panel changes only the end date. A click on a day before the start date, or any click in the left panel, starts a new range.
|
|
211
|
+
- Keyboard focus in a panel's day grid stays inside that panel's visible month; use the panel's arrows or month/year menus to change month.
|
|
211
212
|
|
|
212
213
|
### CalendarChips
|
|
213
214
|
|
|
@@ -229,20 +230,21 @@ interface CalendarChipOption {
|
|
|
229
230
|
|
|
230
231
|
### CalendarGrid
|
|
231
232
|
|
|
232
|
-
| Prop | Type
|
|
233
|
-
| :------------- |
|
|
234
|
-
| currentMonth | `Date`
|
|
235
|
-
| locale | `string`
|
|
236
|
-
| firstDayOfWeek | `number`
|
|
237
|
-
| selectsRange | `boolean`
|
|
238
|
-
| minDate | `Date \| null`
|
|
239
|
-
| maxDate | `Date \| null`
|
|
240
|
-
| startDate | `Date \| null`
|
|
241
|
-
| endDate | `Date \| null`
|
|
242
|
-
| selectedDate | `Date \| null`
|
|
243
|
-
| selectingRange | `Date \| null`
|
|
244
|
-
| onDayPress | `(date: Date) => void`
|
|
245
|
-
|
|
|
233
|
+
| Prop | Type | Default | Description |
|
|
234
|
+
| :------------- | :---------------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------- |
|
|
235
|
+
| currentMonth | `Date` | required | The month to render. |
|
|
236
|
+
| locale | `string` | `"enUS"` | Date-fns locale identifier. |
|
|
237
|
+
| firstDayOfWeek | `number` | `0` | First day of week. |
|
|
238
|
+
| selectsRange | `boolean` | `false` | Enable range mode. |
|
|
239
|
+
| minDate | `Date \| null` | - | Minimum selectable date. |
|
|
240
|
+
| maxDate | `Date \| null` | - | Maximum selectable date. |
|
|
241
|
+
| startDate | `Date \| null` | - | Range start date. |
|
|
242
|
+
| endDate | `Date \| null` | - | Range end date. |
|
|
243
|
+
| selectedDate | `Date \| null` | - | Selected date (single mode). |
|
|
244
|
+
| selectingRange | `Date \| null` | - | Intermediate range selection. |
|
|
245
|
+
| onDayPress | `(date: Date) => void` | - | Day press callback. Also fired by Enter / Space on the focused day. |
|
|
246
|
+
| onMonthChange | `(month: Date) => void` | - | Called with the first day of a month when keyboard focus moves past the visible month. Without it, focus stays in the month. |
|
|
247
|
+
| testID | `string` | - | Test identifier. |
|
|
246
248
|
|
|
247
249
|
## Platform Support
|
|
248
250
|
|
|
@@ -256,6 +258,21 @@ This package works on both **web** and **React Native**. All components use cros
|
|
|
256
258
|
## Accessibility
|
|
257
259
|
|
|
258
260
|
- Navigation buttons have aria-labels ("Previous month", "Next month")
|
|
259
|
-
- Month and year selectors are
|
|
260
|
-
-
|
|
261
|
+
- Month and year selectors are Select components: each is a tab stop and opens, navigates and selects with the keyboard (see the Select docs)
|
|
262
|
+
- The day grid has `role="grid"`, week rows `role="row"`, weekday labels `role="columnheader"` and days `role="gridcell"`
|
|
263
|
+
- Each day in the visible month has an `aria-label` with the full date (e.g. "Thursday, 15 January 2026"), plus `aria-selected`, `aria-disabled` for days outside `minDate` / `maxDate`, and `aria-current="date"` for today
|
|
261
264
|
- Chip selection follows radio-group pattern (single active item)
|
|
265
|
+
|
|
266
|
+
### Keyboard (web)
|
|
267
|
+
|
|
268
|
+
Tab order: Previous month → month menu → year menu → Next month → day grid. The day grid is a single tab stop (roving `tabIndex`): the selected day (or the range start), else today, else the first day of the month.
|
|
269
|
+
|
|
270
|
+
| Key | Action |
|
|
271
|
+
| :-- | :----- |
|
|
272
|
+
| ← / → | Previous / next day |
|
|
273
|
+
| ↑ / ↓ | Same day, previous / next week |
|
|
274
|
+
| Home / End | First / last day of the week |
|
|
275
|
+
| PageUp / PageDown | Same day, previous / next month |
|
|
276
|
+
| Enter / Space | Selects the focused day (no effect on a disabled day) |
|
|
277
|
+
|
|
278
|
+
When focus moves past the visible month, Calendar switches to that month. In DualCalendar, focus stays inside each panel's month.
|
package/native/index.d.mts
CHANGED
|
@@ -40,6 +40,12 @@ interface CalendarGridProps extends ThemeOverrideProps {
|
|
|
40
40
|
selectedDate?: Date | null;
|
|
41
41
|
selectingRange?: Date | null;
|
|
42
42
|
onDayPress?: (date: Date) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Called with the first day of a month when keyboard navigation moves past
|
|
45
|
+
* the visible month (arrow keys, PageUp/PageDown). Without it, keyboard
|
|
46
|
+
* focus stays inside the visible month.
|
|
47
|
+
*/
|
|
48
|
+
onMonthChange?: (month: Date) => void;
|
|
43
49
|
testID?: string;
|
|
44
50
|
}
|
|
45
51
|
interface CalendarHeaderProps extends ThemeOverrideProps {
|
package/native/index.d.ts
CHANGED
|
@@ -40,6 +40,12 @@ interface CalendarGridProps extends ThemeOverrideProps {
|
|
|
40
40
|
selectedDate?: Date | null;
|
|
41
41
|
selectingRange?: Date | null;
|
|
42
42
|
onDayPress?: (date: Date) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Called with the first day of a month when keyboard navigation moves past
|
|
45
|
+
* the visible month (arrow keys, PageUp/PageDown). Without it, keyboard
|
|
46
|
+
* focus stays inside the visible month.
|
|
47
|
+
*/
|
|
48
|
+
onMonthChange?: (month: Date) => void;
|
|
43
49
|
testID?: string;
|
|
44
50
|
}
|
|
45
51
|
interface CalendarHeaderProps extends ThemeOverrideProps {
|
package/native/index.js
CHANGED
|
@@ -348,6 +348,9 @@ var Text = ({
|
|
|
348
348
|
);
|
|
349
349
|
};
|
|
350
350
|
|
|
351
|
+
// ../../foundation/primitives-native/src/index.tsx
|
|
352
|
+
var isWeb = false;
|
|
353
|
+
|
|
351
354
|
// src/Calendar.tsx
|
|
352
355
|
var import_xui_core3 = require("@xsolla/xui-core");
|
|
353
356
|
var import_date_fns3 = require("date-fns");
|
|
@@ -509,6 +512,7 @@ var locales2 = __toESM(require("date-fns/locale"));
|
|
|
509
512
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
510
513
|
var DAY_CELL_WIDTH = 40;
|
|
511
514
|
var GRID_WIDTH = DAY_CELL_WIDTH * 7;
|
|
515
|
+
var dayKeyOf = (date) => (0, import_date_fns2.format)(date, "yyyy-MM-dd");
|
|
512
516
|
var CalendarGrid = ({
|
|
513
517
|
currentMonth,
|
|
514
518
|
locale = "enUS",
|
|
@@ -521,6 +525,7 @@ var CalendarGrid = ({
|
|
|
521
525
|
selectedDate,
|
|
522
526
|
selectingRange,
|
|
523
527
|
onDayPress,
|
|
528
|
+
onMonthChange,
|
|
524
529
|
testID,
|
|
525
530
|
themeMode,
|
|
526
531
|
themeProductContext
|
|
@@ -569,33 +574,122 @@ var CalendarGrid = ({
|
|
|
569
574
|
},
|
|
570
575
|
[selectsRange, startDate, endDate, selectingRange]
|
|
571
576
|
);
|
|
572
|
-
|
|
577
|
+
const [focusedDate, setFocusedDate] = (0, import_react2.useState)(null);
|
|
578
|
+
const dayRefs = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
|
|
579
|
+
const shouldMoveFocus = (0, import_react2.useRef)(false);
|
|
580
|
+
const tabStopDate = (0, import_react2.useMemo)(() => {
|
|
581
|
+
if (focusedDate && (0, import_date_fns2.isSameMonth)(focusedDate, currentMonth)) {
|
|
582
|
+
return focusedDate;
|
|
583
|
+
}
|
|
584
|
+
const preferred = selectsRange ? startDate : selectedDate;
|
|
585
|
+
if (preferred && (0, import_date_fns2.isSameMonth)(preferred, currentMonth)) return preferred;
|
|
586
|
+
const today = /* @__PURE__ */ new Date();
|
|
587
|
+
if ((0, import_date_fns2.isSameMonth)(today, currentMonth)) return today;
|
|
588
|
+
return monthStart;
|
|
589
|
+
}, [
|
|
590
|
+
focusedDate,
|
|
591
|
+
currentMonth,
|
|
592
|
+
selectsRange,
|
|
593
|
+
startDate,
|
|
594
|
+
selectedDate,
|
|
595
|
+
monthStart
|
|
596
|
+
]);
|
|
597
|
+
const tabStopKey = dayKeyOf(tabStopDate);
|
|
598
|
+
(0, import_react2.useEffect)(() => {
|
|
599
|
+
if (!shouldMoveFocus.current) return;
|
|
600
|
+
shouldMoveFocus.current = false;
|
|
601
|
+
dayRefs.current.get(tabStopKey)?.focus();
|
|
602
|
+
}, [tabStopKey, currentMonth]);
|
|
603
|
+
const moveFocus = (0, import_react2.useCallback)(
|
|
604
|
+
(target) => {
|
|
605
|
+
if (!(0, import_date_fns2.isSameMonth)(target, currentMonth)) {
|
|
606
|
+
if (!onMonthChange) return;
|
|
607
|
+
onMonthChange((0, import_date_fns2.startOfMonth)(target));
|
|
608
|
+
}
|
|
609
|
+
shouldMoveFocus.current = true;
|
|
610
|
+
setFocusedDate(target);
|
|
611
|
+
},
|
|
612
|
+
[currentMonth, onMonthChange]
|
|
613
|
+
);
|
|
614
|
+
const handleDayKeyDown = (0, import_react2.useCallback)(
|
|
615
|
+
(event, day) => {
|
|
616
|
+
let target = null;
|
|
617
|
+
switch (event.key) {
|
|
618
|
+
case "ArrowLeft":
|
|
619
|
+
target = (0, import_date_fns2.addDays)(day, -1);
|
|
620
|
+
break;
|
|
621
|
+
case "ArrowRight":
|
|
622
|
+
target = (0, import_date_fns2.addDays)(day, 1);
|
|
623
|
+
break;
|
|
624
|
+
case "ArrowUp":
|
|
625
|
+
target = (0, import_date_fns2.addDays)(day, -7);
|
|
626
|
+
break;
|
|
627
|
+
case "ArrowDown":
|
|
628
|
+
target = (0, import_date_fns2.addDays)(day, 7);
|
|
629
|
+
break;
|
|
630
|
+
case "Home":
|
|
631
|
+
target = (0, import_date_fns2.startOfWeek)(day, { weekStartsOn });
|
|
632
|
+
break;
|
|
633
|
+
case "End":
|
|
634
|
+
target = (0, import_date_fns2.endOfWeek)(day, { weekStartsOn });
|
|
635
|
+
break;
|
|
636
|
+
case "PageUp":
|
|
637
|
+
target = (0, import_date_fns2.addMonths)(day, -1);
|
|
638
|
+
break;
|
|
639
|
+
case "PageDown":
|
|
640
|
+
target = (0, import_date_fns2.addMonths)(day, 1);
|
|
641
|
+
break;
|
|
642
|
+
case "Enter":
|
|
643
|
+
case " ":
|
|
644
|
+
event.preventDefault();
|
|
645
|
+
if (!isDateDisabled(day)) onDayPress?.(day);
|
|
646
|
+
return;
|
|
647
|
+
default:
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
event.preventDefault();
|
|
651
|
+
moveFocus(target);
|
|
652
|
+
},
|
|
653
|
+
[isDateDisabled, moveFocus, onDayPress, weekStartsOn]
|
|
654
|
+
);
|
|
655
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box, { testID, role: "grid", children: [
|
|
573
656
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
574
657
|
Box,
|
|
575
658
|
{
|
|
659
|
+
role: "row",
|
|
576
660
|
flexDirection: "row",
|
|
577
661
|
justifyContent: "flex-start",
|
|
578
662
|
width: GRID_WIDTH,
|
|
579
663
|
marginBottom: 8,
|
|
580
|
-
children: weekDays.map((day, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
581
|
-
|
|
664
|
+
children: weekDays.map((day, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
665
|
+
Box,
|
|
582
666
|
{
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
children:
|
|
587
|
-
|
|
588
|
-
|
|
667
|
+
role: "columnheader",
|
|
668
|
+
width: DAY_CELL_WIDTH,
|
|
669
|
+
alignItems: "center",
|
|
670
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
671
|
+
Text,
|
|
672
|
+
{
|
|
673
|
+
fontSize: 12,
|
|
674
|
+
fontWeight: "600",
|
|
675
|
+
color: theme.colors.content.tertiary,
|
|
676
|
+
children: day.toUpperCase()
|
|
677
|
+
}
|
|
678
|
+
)
|
|
679
|
+
},
|
|
680
|
+
i
|
|
681
|
+
))
|
|
589
682
|
}
|
|
590
683
|
),
|
|
591
684
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box, { gap: 2, children: weeks.map((week, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
592
685
|
Box,
|
|
593
686
|
{
|
|
687
|
+
role: "row",
|
|
594
688
|
flexDirection: "row",
|
|
595
689
|
justifyContent: "flex-start",
|
|
596
690
|
width: GRID_WIDTH,
|
|
597
691
|
children: week.map((day, j) => {
|
|
598
|
-
const dayKey = (
|
|
692
|
+
const dayKey = dayKeyOf(day);
|
|
599
693
|
const isOutsideMonth = !(0, import_date_fns2.isSameMonth)(day, currentMonth);
|
|
600
694
|
const isSelected = !selectsRange && selectedDate && (0, import_date_fns2.isSameDay)(day, selectedDate) || selectsRange && startDate && endDate && (0, import_date_fns2.isSameDay)(day, startDate) && (0, import_date_fns2.isSameDay)(day, endDate);
|
|
601
695
|
const isRangeStart = selectsRange && startDate && (0, import_date_fns2.isSameDay)(day, startDate);
|
|
@@ -605,6 +699,7 @@ var CalendarGrid = ({
|
|
|
605
699
|
const disabled = isDateDisabled(day);
|
|
606
700
|
const isHoverable = !disabled && !isOutsideMonth && !isSelected && !isRangeStart && !isRangeEnd;
|
|
607
701
|
const isHovered = isHoverable && hoveredKey === dayKey;
|
|
702
|
+
const isTabStop = !isOutsideMonth && dayKey === tabStopKey;
|
|
608
703
|
let bgColor = "transparent";
|
|
609
704
|
let textColor = theme.colors.content.primary;
|
|
610
705
|
if (isSelected || isRangeStart || isRangeEnd || inRange) {
|
|
@@ -616,9 +711,23 @@ var CalendarGrid = ({
|
|
|
616
711
|
} else if (isOutsideMonth || disabled) {
|
|
617
712
|
textColor = theme.colors.control.text.disable;
|
|
618
713
|
}
|
|
714
|
+
const keyboardProps = isWeb && !isOutsideMonth ? {
|
|
715
|
+
ref: (node) => {
|
|
716
|
+
if (node) dayRefs.current.set(dayKey, node);
|
|
717
|
+
else dayRefs.current.delete(dayKey);
|
|
718
|
+
},
|
|
719
|
+
tabIndex: isTabStop ? 0 : -1,
|
|
720
|
+
onKeyDown: (event) => handleDayKeyDown(event, day)
|
|
721
|
+
} : {};
|
|
619
722
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
620
723
|
Box,
|
|
621
724
|
{
|
|
725
|
+
...keyboardProps,
|
|
726
|
+
role: "gridcell",
|
|
727
|
+
"aria-label": isOutsideMonth ? void 0 : (0, import_date_fns2.format)(day, "EEEE, d MMMM yyyy", { locale: localeObj }),
|
|
728
|
+
"aria-selected": isOutsideMonth ? void 0 : Boolean(isSelected || isRangeStart || isRangeEnd),
|
|
729
|
+
"aria-disabled": disabled || isOutsideMonth || void 0,
|
|
730
|
+
"aria-current": today ? "date" : void 0,
|
|
622
731
|
width: DAY_CELL_WIDTH,
|
|
623
732
|
height: 32,
|
|
624
733
|
alignItems: "center",
|
|
@@ -629,7 +738,11 @@ var CalendarGrid = ({
|
|
|
629
738
|
borderBottomRightRadius: isSelected || isRangeEnd ? 4 : 0,
|
|
630
739
|
backgroundColor: bgColor,
|
|
631
740
|
cursor: disabled || isOutsideMonth ? "default" : "pointer",
|
|
632
|
-
onPress: () =>
|
|
741
|
+
onPress: () => {
|
|
742
|
+
if (disabled || isOutsideMonth) return;
|
|
743
|
+
setFocusedDate(day);
|
|
744
|
+
onDayPress?.(day);
|
|
745
|
+
},
|
|
633
746
|
onMouseEnter: () => isHoverable && setHoveredKey(dayKey),
|
|
634
747
|
onMouseLeave: () => setHoveredKey(
|
|
635
748
|
(current) => current === dayKey ? null : current
|
|
@@ -897,6 +1010,7 @@ var Calendar = (0, import_react3.forwardRef)(
|
|
|
897
1010
|
selectedDate: locSelectedDate,
|
|
898
1011
|
selectingRange,
|
|
899
1012
|
onDayPress: handleChange,
|
|
1013
|
+
onMonthChange: setCurrentMonth,
|
|
900
1014
|
themeMode,
|
|
901
1015
|
themeProductContext
|
|
902
1016
|
}
|