/** * Datepicker field widget types. * * The committed value is an ISO `YYYY-MM-DD` string or `null`. Which month is on screen * (`{viewYear, viewMonth}`) and which cell the keyboard is on (`focusedDate`) are view state kept * apart from it, so paging and moving focus change what is shown without selecting anything. * Keyboard navigation resolves through `calendarKeyboardTarget` (`@modyra/widgets`). * * Month and year drill-down views, and a confirm/cancel draft, belong to the host — see * datepicker-field-controller.ts. */ import type { MdyInteractivity } from "@modyra/core"; import type { MdyCalendarViewMode } from "./calendar-view.js"; import type { MdyFieldHandle } from "@modyra/core"; export interface MdyDatepickerFieldControllerOptions { /** Stable identity for the widget instance. */ readonly widgetId: string; /** Form engine handle; value is an ISO `YYYY-MM-DD` string or null. */ readonly handle: MdyFieldHandle; /** Inclusive lower bound, ISO `YYYY-MM-DD`. */ readonly minDate?: string | null; /** Inclusive upper bound, ISO `YYYY-MM-DD`. */ readonly maxDate?: string | null; /** 0 = Sunday (default), 1 = Monday, … — pass `locale.firstDayOfWeek` from `@modyra/core/datetime` for a real locale. */ readonly firstDayOfWeek?: number; /** Whether the widget is visually/programmatically readonly. */ readonly readonly?: boolean; /** * Reads typed text as an ISO date, or answers null when it cannot. * * A dependency because the reading is locale-aware and the locale belongs to the host: a control * knows what `14/03` means where it is rendered, and this package does not. Without one, a typed * entry is left alone. */ readonly parseEntry?: (text: string) => string | null; } /** One rendered calendar cell — same shape `buildMonthGrid` already produces, so the controller can hand it straight to the host. */ export interface MdyDatepickerFieldCell { readonly iso: string; readonly day: number; readonly inMonth: boolean; readonly selected: boolean; readonly focused: boolean; readonly disabled: boolean; } /** Semantic state of a datepicker field widget. */ export interface MdyDatepickerFieldState { readonly selectedDate: string | null; /** Days, months or years — what the popup is showing. See {@link MdyCalendarViewMode}. */ readonly viewMode: MdyCalendarViewMode; readonly viewYear: number; readonly viewMonth: number; readonly focusedDate: string; readonly cells: ReadonlyArray; readonly open: boolean; readonly invalid: boolean; readonly disabled: boolean; /** * What the user may do. Ask it through `blocksValueChange`/`blocksFocus` rather than comparing * strings — the point of the union is that no call site invents its own combination again. */ readonly interactivity: MdyInteractivity; readonly readonly: boolean; readonly required: boolean; readonly touched: boolean; readonly dirty: boolean; readonly pending: boolean; /** * What the person typed, while it is not a date this field can hold. * * A control renders this in place of the formatted value, so an entry that could not be read stays * on screen and can be corrected. Null while there is nothing outstanding — which is every state * except the one immediately after an unreadable entry. */ readonly entryText: string | null; /** * Whether the outstanding entry could not be read. * * Separate from `entryText` being present because it is the half a control shows a *verdict* for: * keeping the text without saying anything leaves a field that looks accepted holding a value it * never took, which is what `acceptTimeField` refuses one level down. */ readonly entryUnreadable: boolean; } /** User/host intent for a datepicker field widget. */ export type MdyDatepickerFieldIntent = { readonly type: "open"; } | { readonly type: "close"; readonly restoreFocus?: boolean; } | { readonly type: "navigate-month"; readonly delta: number; } | { readonly type: "set-view-mode"; readonly mode: MdyCalendarViewMode; } /** Choose a month from the month view; narrows to its days. */ | { readonly type: "select-month"; readonly month: number; } /** Choose a year from the year view; narrows to its months. */ | { readonly type: "select-year"; readonly year: number; } | { readonly type: "keydown"; readonly key: string; readonly shiftKey?: boolean; /** * Whether the platform's accelerator was held with the press. * * Carried because the calendar decides what a key does, and without this it decided as though * every press were bare: `Cmd`+ArrowDown reaches the end of a document and `Cmd`+Enter submits * a form in half the products people use, and both also moved or committed a date. A gesture * that *adds* something does not answer a press that may have been aimed elsewhere; `Escape` * does, because refusing a dismissal leaves somebody inside a panel with the way out shut. */ readonly ctrlKey?: boolean; readonly metaKey?: boolean; } | { readonly type: "select-date"; readonly iso: string; } /** * The person typed something and left the control. * * The control hands over the text rather than parsing it, so both renderers answer a typed entry * the same way — parsing in each of them is why they could differ at all. */ | { readonly type: "type"; readonly text: string; } | { readonly type: "clear"; } | { readonly type: "focus"; } | { readonly type: "blur"; };