Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions website/content/docs/accessibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
title: Accessibility
description: Keyboard interactions, ARIA semantics, and localization built into $projectName.
order: 3
section: Overview
---

{% $projectName %} implements the
[ARIA grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) for calendars, so the
markup you compose is accessible before you write a line of ARIA yourself. This page
describes what the library does for you — and the few things left in your hands.

## Keyboard interactions

Focus the grid, then:

| Key | Action |
| ------------------------------------- | ------------------------------------------------------------------------ |
| `ArrowRight` / `ArrowLeft` | Move focus one day forward / back |
| `ArrowDown` / `ArrowUp` | Move focus one week forward / back |
| `Home` / `End` | Move focus to the first / last day of the week (respects `weekStartDay`) |
| `PageDown` / `PageUp` | Move focus one month forward / back |
| `Shift + PageDown` / `Shift + PageUp` | Move focus one year forward / back |
| `Enter` / `Space` | Select the focused day |

Movement is clamped to your `min`/`max` bounds — focus stops at the boundary instead of
escaping the selectable window. When focus crosses a month boundary, the view follows
automatically (MonthView pages; WeeksView scrolls its window).

In a `readOnly` calendar, navigation still works but `Enter`/`Space` do nothing. In a
`disabled` calendar, keyboard interaction is off entirely.

## Focus management

The grid uses a **roving tab index**: exactly one day cell is in the tab order at a time,
so the calendar occupies a single tab stop in the page. The tab target is the selected
day when there is one, otherwise today, otherwise the nearest sensible day in view.

Pass `autoFocus` to `Grid` to move DOM focus into the grid when it mounts — useful when
the calendar opens inside a popover:

```tsx
<Grid autoFocus>{/* … */}</Grid>
```

## Semantics and labelling

- `Grid` renders a `<table role="grid">`; `DayCellTemplate` renders
`<td role="gridcell">` cells with `aria-selected` and `aria-disabled` reflecting state.
- Each `DayButton` gets a full-date `aria-label` (e.g. "Saturday, June 20, 2026"),
localized with your `locale`.
- `GridHeaderCell` renders abbreviated weekday text with the full weekday name as its
`aria-label`.
- When you render a `MonthYearString`, the grid is automatically linked to it with
`aria-labelledby`, giving screen-reader users the "June 2026" context. Without one, the
grid falls back to `aria-label="Calendar"`.
- `MonthYearString`, `DateString`, and `TimeString` render with `aria-live="polite"`, so
month navigation and selection changes are announced without stealing focus.
- `PrevMonthButton` / `NextMonthButton` (and the WeeksView equivalents) carry descriptive
`aria-label`s and use the native `disabled` attribute at bounds, so assistive tech sees
real button semantics.

## Outside and hidden days

`outsideDays="hidden"` keeps the grid shape intact for assistive tech: hidden cells stay in
the DOM as empty `<td>` elements with `aria-hidden` (and a `data-hidden` styling hook)
rather than being removed, so row and column counts remain consistent.

## Range drag handles

`RangeStartDragHandle` / `RangeEndDragHandle` are pointer affordances layered on top of the
keyboard-accessible selection model. They expose `aria-roledescription="drag handle"`, a
label for the boundary they control, and `aria-valuetext` with the current date — and they
are `aria-hidden` while inactive. Because every range operation can also be performed by
clicking or with the keyboard, dragging is an enhancement, not a requirement.

## Localization

- **`locale`** (BCP 47 string, default `"en-US"`) drives every formatted string: weekday
headers, month/year labels, and day `aria-label`s, all via `Intl.DateTimeFormat`.
- **`weekStartDay`** (0 = Sunday … 6 = Saturday) reorders the grid _and_ the `Home`/`End`
keyboard behavior together, so visual and keyboard order never disagree.
- **`timeZone`** (IANA identifier) controls which day is "today" and how values convert.
See [Dates &amp; formats](/docs/dates-and-formats).

```tsx
<MonthView temporal={Temporal} locale="de-DE" weekStartDay={1}>
{/* Mo Di Mi Do Fr Sa So */}
</MonthView>
```

## Your responsibilities

Headless means a few things remain yours:

- **Visible focus.** Style `:focus-visible` on `DayButton` (and the nav buttons) with a
clearly visible ring; the library manages _where_ focus goes, you make it _seen_.
- **Color contrast.** Selected, in-range, disabled, and outside-month states are your
colors — keep them WCAG-compliant, and don't rely on color alone to convey selection.
- **Hit targets.** Keep day buttons comfortably tappable (44×44 px is a good floor) if you
target touch devices.
88 changes: 84 additions & 4 deletions website/content/docs/calendar-provider.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,98 @@
---
title: CalendarProvider
description: Manages shared state across calendar views — selection, bounds, locale, and more.
order: 2
order: 20
section: Components
---

## Props
`CalendarProvider` is the state root of every calendar. It owns the selection (in all
[three modes](/docs/selection-modes)), resolves configuration — bounds, time zone, locale,
week start, the [Temporal implementation](/docs/dates-and-formats) — and shares everything
with its descendants through context. It renders no DOM of its own.

```tsx
import { Temporal } from "@js-temporal/polyfill";
import { CalendarProvider, MonthView } from "@klinking/colander";

<CalendarProvider
temporal={Temporal}
selectionMode="range"
weekStartDay={1}
onValueChange={(range) => console.log(range)}
>
<MonthView.Root>{/* navigation + grid */}</MonthView.Root>
</CalendarProvider>;
```

## When you need it explicitly

The `MonthView` and `WeeksView` wrappers already include a `CalendarProvider`, so most
calendars never mention it. Reach for the explicit form when:

- **Components outside the view need calendar state.** Anything using
`useCalendarStable()` / `useCalendarState()` must live under the provider — a selection
summary, preset-range buttons, a clear button.
- **Two views should share one selection.** Render both roots under a single provider —
for example a month grid next to a scrolling weeks strip, always in sync:

```tsx
<CalendarProvider temporal={Temporal} selectionMode="range">
<MonthView.Root>{/* … */}</MonthView.Root>
<WeeksView.Root weekCount={3}>{/* … */}</WeeksView.Root>
</CalendarProvider>
```

- **You're building a custom view** from the grid primitives and hooks.

{% callout type="warning" %}
Don't nest a `MonthView`/`WeeksView` _wrapper_ inside a `CalendarProvider` — the wrapper
creates its own provider, which would shadow yours. Inside an explicit provider, always use
`MonthView.Root` / `WeeksView.Root`.
{% /callout %}

## What it manages

- **Selection** — single / range / multiple, controlled or uncontrolled, exposed in your
configured value [format](/docs/dates-and-formats).
- **Constraints** — `min`, `max`, `isDateDisabled`, `disabled`, `readOnly`.
- **Range behavior** — `rangeMode`, `preventRangeReversal`, the hover preview and
`previewRange` override.
- **Environment** — `temporal`, `timeZone`, `locale`, `weekStartDay`.

What it does _not_ manage: focus, keyboard navigation, and which month/weeks are visible —
those belong to the view roots ([MonthView](/docs/month-view), [WeeksView](/docs/weeks-view)),
which is why the provider alone renders nothing interactive.

## Reading state with hooks

Two hooks expose the provider's context, split by volatility so components can subscribe
to only what they need:

- `useCalendarStable()` — configuration and stable callbacks (`onSelect`, `setRange`,
`selectionMode`, `minValue`, `maxValue`, `temporal`, `locale`, `timeZone`, …). Doesn't
change during normal interaction.
- `useCalendarState()` — the live values (`selected`, `selectedDates`, `rangeStart`,
`rangeEnd`, `hoveredDate`, `previewStart`, `previewEnd`).

```tsx
function ClearButton() {
const { setRange, readOnly } = useCalendarStable();
const { rangeStart, rangeEnd } = useCalendarState();
if (!rangeStart || readOnly) return null;
// …
}
```

## API reference

### Props

{% api symbol="CalendarProviderProps" /%}

## Stable Context
### Stable context

{% api symbol="CalendarStableContextValue" /%}

## State Context
### State context

{% api symbol="CalendarStateContextValue" /%}
156 changes: 156 additions & 0 deletions website/content/docs/composition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
---
title: Composition
description: How $projectName components fit together — templates, the render prop, and building your own parts.
order: 10
section: Guides
---

{% $projectName %} is a set of compound components: small parts that you arrange in JSX to
form a calendar. This page explains the component tree, the _template_ components that
repeat themselves, the `render` prop, and how to drop down to hooks when you need a part
the library doesn't ship.

## The component tree

```text
CalendarProvider selection state, bounds, locale, time zone
└─ MonthView.Root │ WeeksView.Root view state: visible month(s) / week window, focus
├─ PrevMonthButton / NextMonthButton (or PrevWeeksButton / NextWeeksButton)
├─ MonthYearString localized label, labels the grid
└─ Grid <table role="grid">
├─ GridHeader <thead>
│ └─ GridHeaderCell <th> weekday labels (×7)
└─ GridBody <tbody>
└─ WeekTemplate <tr> — repeated per visible week
├─ WeekNumberCell optional <td> week number
├─ RangeSelected optional range overlay <td>
├─ RangePreview optional hover-preview overlay <td>
└─ DayCellTemplate <td role="gridcell"> — repeated per day
└─ DayButton <button> — the interactive day
├─ RangeStartDragHandle optional
└─ RangeEndDragHandle optional
```

Two rules make the tree work:

1. **State flows down through context.** `CalendarProvider` owns the selection;
the view root owns navigation and focus; grid parts read both. No prop drilling.
2. **You write structure once; templates repeat it.**

## Templates

`WeekTemplate`, `DayCellTemplate`, and `GridHeaderCell` are _templates_: the single element
you write is instantiated for every week, day, and weekday in view. This keeps a full
calendar's JSX to a dozen lines while letting you customize the one repeated unit:

```tsx
<GridBody>
<WeekTemplate>
<DayCellTemplate>
<DayButton className="day" />
</DayCellTemplate>
</WeekTemplate>
</GridBody>
```

Each instance receives its own state — `DayCellTemplate` and `DayButton` know their `date`,
`columnIndex`, and every selection flag for that specific day. You can also opt out of
iteration: `DayCellTemplate date={someDate}` renders a single explicit cell, and
`GridHeaderCell index={0}` renders just one weekday header.

## Convenience wrappers vs. explicit provider

`MonthView` and `WeeksView` fold a `CalendarProvider` and the corresponding `.Root` into
one component — ideal for the common case:

```tsx
<MonthView temporal={Temporal} selectionMode="range">
{/* navigation + grid */}
</MonthView>
```

Use the explicit form when you need to place the provider yourself — for example, to share
one selection between two views, or to read calendar state from components that live
outside the view:

```tsx
<CalendarProvider temporal={Temporal} selectionMode="range">
<MonthView.Root numberOfMonths={2}>{/* grids */}</MonthView.Root>
<SelectionSummary /> {/* reads state via hooks, outside the view */}
</CalendarProvider>
```

Both forms accept the same props; the wrapper simply forwards view props to `.Root` and
everything else to the provider.

## The render prop

Every component accepts a `render` prop (the same pattern as
[Base UI](https://base-ui.com/react/handbook/composition)) for when `className` and CSS
aren't enough. Pass a function that receives the merged DOM props and a typed `state`
object, and return the element you want:

```tsx
<WeekTemplate
render={(props, state) => (
<tr
{...props}
style={state.gridRowIndex ? { gridRow: state.gridRowIndex } : undefined}
/>
)}
/>
```

Rules of thumb:

- **Always spread `props`** onto your element — they carry the event handlers, ARIA
attributes, and `data-*` state the library manages.
- **`state` is read-only and fully typed** per component (`DayButtonState`,
`WeekTemplateState`, `RangeSelectedState`, …). Every state also includes `state.root`
(a `RootState`) with calendar-wide values: `selected`, `rangeStart`/`rangeEnd`,
`viewing`, `focused`, `locale`, `timeZone`, and more.
- Keep the element type semantically equivalent (a `<td>` for cell parts, a `<button>` for
interactive parts) so the grid semantics survive.

## Building your own parts

When the shipped parts aren't enough, use the same hooks and contexts they're built from:

- `useCalendarStable()` — stable config and actions: `onSelect(date)`,
`setRange(start, end)`, `selectionMode`, `minValue`/`maxValue`, `temporal`, `locale`,
`timeZone`, `weekStartDay`.
- `useCalendarState()` — live selection state: `selected`, `selectedDates`,
`rangeStart`/`rangeEnd`, `hoveredDate`, `previewStart`/`previewEnd`.
- `useMonthViewState()` / `useWeeksViewState()` — view state: the visible month data
(`allMonths`) or the weeks window (`windowInfo`).
- `DayCellDataContext` / `GridContext` — inside a cell, the cell's `date` and the grid's
`orientation`.

For example, a footer that shows the current selection:

```tsx
function SelectionSummary() {
const { locale } = useCalendarStable();
const { rangeStart, rangeEnd } = useCalendarState();
if (!rangeStart) return <p>Select a start date</p>;
return (
<p>
{rangeStart.toLocaleString(locale)}
{rangeEnd ? ` – ${rangeEnd.toLocaleString(locale)}` : " – …"}
</p>
);
}
```

Any component using these hooks must be rendered inside the provider (and inside the view
root for the view hooks).

## Multiple months

One `MonthView.Root` can display several consecutive months. Set `numberOfMonths`, then
render one `Grid` per month with `monthIndex`:

{% example file="multiple-months.tsx" /%}

`MonthYearString monthIndex={i}` labels each grid; navigation buttons page the whole
window. See [MonthView](/docs/month-view) for details.
Loading
Loading