# Calendar

A cross-platform React calendar component supporting single date selection, date range selection, preset date chips, and a dual (side-by-side) calendar layout.
<!-- BEGIN:xui-mcp-instructions:calendar -->
xui-mcp-instructions:calendarA panel component that displays an interactive date grid for selecting a single date or a date range. Calendar is always triggered by a DatePicker field — it does not appear on its own.

### When to use
- As the date selection panel opened by a DatePicker input
- When a user needs to pick a date by navigating a calendar view
- When selecting a date range spanning days, weeks, or months

### Content guidelines
- Chip labels: use standard presets — Today, Yesterday, Last 7 days, Last 30 days, This month, Last month.
- Enable Chips whenever the Calendar is used for filtering or reporting — they significantly speed up selection.
- Use Bottom content for an explicit Apply / Cancel pair when the context requires confirmation before the selection takes effect.

### Behaviour guidelines
- Calendar opens anchored below the DatePicker field; if there is not enough space below, it opens upward.
- Single date (Range=False): clicking a day cell selects it and closes the panel immediately.
- Range (Range=True): first click sets the start date; hovering shows a preview highlight; second click sets the end date and closes the panel. If the second click lands before the first date, the range resets and the clicked date becomes the new start.
- Clicking a chip sets the predefined date or range and closes the panel. The chip enters Active state.
- Pressing Escape closes the panel without changing the selection; focus returns to the DatePicker field.
- The panel renders as a floating overlay — it does not shift page layout.
- Always set Months=Two and Range=True together when using range selection.

### Accessibility
- When the panel opens, focus moves to the selected date cell, or to today's date if nothing is selected.
- When the panel closes, focus returns to the DatePicker field that triggered it.
- Focus is trapped inside the panel while it is open — Tab cycles within the panel only.
- Apply role=*"dialog"* and aria-modal=*"true"* to the Calendar panel container.
- Apply aria-label=*"Choose a date"* (or *"Choose a date range"* when Range=True) to the dialog.
- Apply aria-selected=*"true"* to selected day cells.
- Apply aria-disabled=*"true"* to disabled day cells.
- Apply aria-current=*"date"* to today's date cell.
- Use aria-live to announce month navigation changes to screen readers.
<!-- END:xui-mcp-instructions:calendar -->

## Installation

```bash
npm install @xsolla/xui-calendar
```

## Demo

### Single Date Selection

```tsx
import * as React from "react";
import { Calendar } from "@xsolla/xui-calendar";

export default function SingleDate() {
  const [date, setDate] = React.useState<Date | null>(null);

  return (
    <Calendar
      selectedDate={date}
      onChange={(newDate) => setDate(newDate as Date)}
    />
  );
}
```

### Date Range Selection

```tsx
import * as React from "react";
import { Calendar } from "@xsolla/xui-calendar";

export default function DateRange() {
  const [startDate, setStartDate] = React.useState<Date | null>(null);
  const [endDate, setEndDate] = React.useState<Date | null>(null);

  return (
    <Calendar
      selectsRange
      startDate={startDate}
      endDate={endDate}
      onChange={(range) => {
        const [start, end] = range as [Date | null, Date | null];
        setStartDate(start);
        setEndDate(end);
      }}
    />
  );
}
```

### With Preset Chips

```tsx
import * as React from "react";
import { Calendar } from "@xsolla/xui-calendar";

const chips = [
  { label: "Today", value: "today" },
  { label: "Last 7 days", value: "last7" },
  { label: "Last 30 days", value: "last30" },
  { label: "Last 90 days", value: "last90" },
];

export default function WithChips() {
  const [date, setDate] = React.useState<Date | null>(null);
  const [activeChip, setActiveChip] = React.useState<string | null>("last30");

  return (
    <Calendar
      selectedDate={date}
      onChange={(newDate) => setDate(newDate as Date)}
      chips={chips}
      activeChip={activeChip}
      onChipSelect={setActiveChip}
    />
  );
}
```

### Dual Calendar (Side-by-Side)

```tsx
import * as React from "react";
import { DualCalendar } from "@xsolla/xui-calendar";

export default function DualRange() {
  const [startDate, setStartDate] = React.useState<Date | null>(null);
  const [endDate, setEndDate] = React.useState<Date | null>(null);

  return (
    <DualCalendar
      startDate={startDate}
      endDate={endDate}
      onChange={(dates) => {
        const [start, end] = dates;
        setStartDate(start);
        setEndDate(end);
      }}
    />
  );
}
```

## Anatomy

```
+---------------------------------------+
|  [Custom top-content]                 |  <- topContent slot
|  [Today] [Last 7d] [Last 30d] ...    |  <- CalendarChips
|  <- [Month ▾] [Year ▾] ->            |  <- CalendarHeader
|  SU  MO  TU  WE  TH  FR  SA         |  <- CalendarGrid weekday row
|  29  30  31   1   2   3   4          |
|   5   6   7   8   9  10  11          |
|  12  13  14 [15]  16  17  18         |  <- [15] = today/selected
|  23  20  21  22  23  24  25          |
|  26  27  28  29  30   1   2          |
|  [Custom bottom-content]             |  <- bottomContent slot
+---------------------------------------+
```

## API Reference

### Calendar

| Prop                 | Type                                   | Default  | Description                                                                                                   |
| :------------------- | :------------------------------------- | :------- | :------------------------------------------------------------------------------------------------------------ |
| `testID`             | `string`                               | —        | Test ID for testing frameworks. On web this renders as `data-testid`; on React Native it renders as `testID`. |
| selectedDate         | `Date \| null`                         | -        | Selected date for single mode.                                                                                |
| startDate            | `Date \| null`                         | -        | Start date for range mode.                                                                                    |
| endDate              | `Date \| null`                         | -        | End date for range mode.                                                                                      |
| selectsRange         | `boolean`                              | `false`  | Enable date range selection.                                                                                  |
| onChange             | `(date: Date \| [Date, Date]) => void` | -        | Date change callback.                                                                                         |
| locale               | `string`                               | `"enUS"` | Date-fns locale identifier.                                                                                   |
| firstDayOfWeek       | `number`                               | `0`      | First day of week (0=Sun, 6=Sat).                                                                             |
| initialMonth         | `Date`                                 | -        | Initial month to display.                                                                                     |
| month                | `Date`                                 | -        | Controlled month to display.                                                                                  |
| minDate              | `Date \| null`                         | -        | Minimum selectable date.                                                                                      |
| maxDate              | `Date \| null`                         | -        | Maximum selectable date.                                                                                      |
| chips                | `CalendarChipOption[]`                 | -        | Preset date range chips.                                                                                      |
| activeChip           | `string \| null`                       | -        | Currently active chip value.                                                                                  |
| onChipSelect         | `(value: string) => void`              | -        | Chip selection callback.                                                                                      |
| topContent           | `({ close }) => ReactNode`             | -        | Custom content above chips.                                                                                   |
| bottomContent        | `({ close }) => ReactNode`             | -        | Custom content below grid.                                                                                    |
| contextMenuMaxHeight | `number`                               | -        | Max height for month/year dropdowns.                                                                          |
| testID               | `string`                               | -        | Test identifier.                                                                                              |

### DualCalendar

| Prop                 | Type                            | Default  | Description                          |
| :------------------- | :------------------------------ | :------- | :----------------------------------- |
| startDate            | `Date \| null`                  | -        | Start date of selected range.        |
| endDate              | `Date \| null`                  | -        | End date of selected range.          |
| onChange             | `(dates: [Date, Date]) => void` | -        | Range change callback.               |
| locale               | `string`                        | `"enUS"` | Date-fns locale identifier.          |
| firstDayOfWeek       | `number`                        | `0`      | First day of week (0=Sun, 6=Sat).    |
| initialMonth         | `Date`                          | -        | Initial month for left calendar.     |
| month                | `Date`                          | -        | Controlled month for left calendar.  |
| minDate              | `Date \| null`                  | -        | Minimum selectable date.             |
| maxDate              | `Date \| null`                  | -        | Maximum selectable date.             |
| chips                | `CalendarChipOption[]`          | -        | Shared preset date range chips.      |
| activeChip           | `string \| null`                | -        | Currently active chip value.         |
| onChipSelect         | `(value: string) => void`       | -        | Chip selection callback.             |
| topContent           | `({ close }) => ReactNode`      | -        | Custom content above chips.          |
| bottomContent        | `({ close }) => ReactNode`      | -        | Custom content below grids.          |
| contextMenuMaxHeight | `number`                        | -        | Max height for month/year dropdowns. |
| testID               | `string`                        | -        | Test identifier.                     |

### CalendarChips

| Prop         | Type                      | Default  | Description                  |
| :----------- | :------------------------ | :------- | :--------------------------- |
| chips        | `CalendarChipOption[]`    | required | Array of chip options.       |
| activeChip   | `string \| null`          | -        | Currently active chip value. |
| onChipSelect | `(value: string) => void` | -        | Chip selection callback.     |
| testID       | `string`                  | -        | Test identifier.             |

### CalendarChipOption

```ts
interface CalendarChipOption {
  label: string;
  value: string;
}
```

### CalendarGrid

| Prop           | Type                   | Default  | Description                   |
| :------------- | :--------------------- | :------- | :---------------------------- |
| currentMonth   | `Date`                 | required | The month to render.          |
| locale         | `string`               | `"enUS"` | Date-fns locale identifier.   |
| firstDayOfWeek | `number`               | `0`      | First day of week.            |
| selectsRange   | `boolean`              | `false`  | Enable range mode.            |
| minDate        | `Date \| null`         | -        | Minimum selectable date.      |
| maxDate        | `Date \| null`         | -        | Maximum selectable date.      |
| startDate      | `Date \| null`         | -        | Range start date.             |
| endDate        | `Date \| null`         | -        | Range end date.               |
| selectedDate   | `Date \| null`         | -        | Selected date (single mode).  |
| selectingRange | `Date \| null`         | -        | Intermediate range selection. |
| onDayPress     | `(date: Date) => void` | -        | Day press callback.           |
| testID         | `string`               | -        | Test identifier.              |

## Platform Support

This package works on both **web** and **React Native**. All components use cross-platform primitives (`Box`, `Text`) and avoid DOM-specific APIs.

## Theme

- Calendar and DualCalendar are floating surfaces: the panel background comes from the `layer/float` colour token (`theme.colors.layer.float`), not from a page-level `background/*` token. This keeps the panel visually lifted off the page in both light and dark mode.
- Corner radius comes from `theme.shape.contextMenu.md.borderRadius` so the panel matches ContextMenu and the other overlay surfaces.

## Accessibility

- Navigation buttons have aria-labels ("Previous month", "Next month")
- Month and year selectors are accessible via Select component
- Day cells support keyboard navigation
- Chip selection follows radio-group pattern (single active item)
