import type { KeyboardEvent, ReactNode } from "react" import { useEffect, useRef } from "react" import { FOCUS_RING_CLASS } from "../intentStyles.ts" import { toClassName } from "../toClassName.ts" import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.tsx" import type { PlainDate } from "./plainDate.ts" import { addDays, addMonths, clampPlainDate, comparePlainDates, formatPlainDate, getDaysInMonth, getIsSameDay, getIsWithinRange, getWeekday, getWeekdayNames, toIsoDate, } from "./plainDate.ts" export type DateGridProps = { firstDayOfWeek: number focusedDate: PlainDate /** * The month this grid draws — any date inside it. `DatePicker` * renders two of these in range mode, so the month is a prop * rather than derived from the focus. */ gridMonth: PlainDate /** * Whether the keyboard is *in* the grid. Only the grid that owns * focus pulls it onto its focused cell; the other one would rip * focus back and forth between the two months on every arrow key. */ isGridFocused: boolean isRange: boolean label: string locale?: string maximum?: PlainDate minimum?: PlainDate onFocusDate: (date: PlainDate) => void onHoverDate: (date: null | PlainDate) => void onSelectDate: (date: PlainDate) => void /** * The end of the band being drawn *right now* — the committed end, * or the hovered/keyboard-focused day while the second click is * still outstanding. Painting the pending half is the difference * between a range picker you can aim and one you have to guess at. */ previewEnd: null | PlainDate rangeStart: null | PlainDate selectedDates: readonly PlainDate[] today: PlainDate } const CELL_BASE_CLASS = "cursor-pointer rounded-md p-0 text-center" const DAY_BASE_CLASS = "mx-auto flex size-8 items-center justify-center rounded-md border border-transparent text-xs transition-colors duration-(--duration-fast) ease-standard cq-xs:size-9 cq-xs:text-sm cq-sm:size-10" /** * One month, as a real ``. * * ### Why the roving focus is arithmetic and not `useRovingFocus` * * `@charcuterie/logic`'s `RovingFocus` is a **registration** kind: * members register from an effect and `next`/`previous`/`first`/ * `last` walk that list. `SegmentedControl` and `Tabs` are exactly * that shape, and a calendar is not, for two reasons that are worth * stating rather than discovering: * * - The movement is two-dimensional and it is *date* arithmetic, not * list traversal. ArrowDown is "+7 days", which is a different * thing from "the seventh next registered member" the moment a * month does not start on the first column. * - The move can leave the rendered set entirely. ArrowRight on 31 * August lands on 1 September, which is a day this grid has not * rendered and no member has registered. A registration list * cannot express a move to something that does not exist yet; a * day number can, and re-rendering the grid around it is the * correct response. * * So the focused *date* is the state and the DOM follows it. This is * the same conclusion `Field` reached about `createLinkedIds` — the * state kind is real and this is not its shape. * * ### The cell is the gridcell * * `
` rather than `