diff --git a/website/content/docs/accessibility.md b/website/content/docs/accessibility.md new file mode 100644 index 0000000..2778637 --- /dev/null +++ b/website/content/docs/accessibility.md @@ -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 +{/* … */} +``` + +## Semantics and labelling + +- `Grid` renders a ``; `DayCellTemplate` renders + `
` 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 `` 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 & formats](/docs/dates-and-formats). + +```tsx + + {/* Mo Di Mi Do Fr Sa So */} + +``` + +## 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. diff --git a/website/content/docs/calendar-provider.md b/website/content/docs/calendar-provider.md index 5247baf..624435b 100644 --- a/website/content/docs/calendar-provider.md +++ b/website/content/docs/calendar-provider.md @@ -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"; + + console.log(range)} +> + {/* navigation + grid */} +; +``` + +## 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 + + {/* … */} + {/* … */} + +``` + +- **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" /%} diff --git a/website/content/docs/composition.md b/website/content/docs/composition.md new file mode 100644 index 0000000..58aebee --- /dev/null +++ b/website/content/docs/composition.md @@ -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 + ├─ GridHeader + │ └─ GridHeaderCell + └─ WeekTemplate — repeated per visible week + ├─ WeekNumberCell optional + )} +/> +``` + +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 `
weekday labels (×7) + └─ GridBody
week number + ├─ RangeSelected optional range overlay + ├─ RangePreview optional hover-preview overlay + └─ DayCellTemplate — repeated per day + └─ DayButton
` for cell parts, a `