@alphinex/dates
v1.0.2
Published
Intl-based date/time engine + DatePicker, DateRangePicker, TimePicker, DateTimePicker and the shared month-grid cell engine. No date-fns/dayjs/luxon. See documentation/PRODUCT-RESEARCH-MASTER-PLAN.md §10.3 (Sprint 37).
Readme
@alphinex/dates
Date and time for the Alphinex platform: an Intl-based engine, a month-grid cell engine, and
four pickers built on top of them.
No date-fns, no dayjs, no luxon. Everything here is native Date plus Intl, so adding
a date field to your app adds no third-party date library to your bundle.
pnpm add @alphinex/datesRequires a <ThemeProvider> above it (the grids read dir to mirror their arrow-key axis under
RTL), the same as Slider and Tooltip.
Quick start
import { ThemeProvider } from "@alphinex/theme";
import { DatePicker } from "@alphinex/dates";
import { useState } from "react";
function InvoiceForm() {
const [dueAt, setDueAt] = useState<Date | null>(null);
return (
<ThemeProvider>
<DatePicker
aria-label="Due date"
value={dueAt}
onChange={setDueAt}
placeholder="Pick a date"
/>
</ThemeProvider>
);
}Components
DatePicker
A text field that accepts a typed date and opens a calendar.
<DatePicker
aria-label="Due date"
value={dueAt}
onChange={setDueAt}
min={new Date()} // nothing in the past
disabledDaysOfWeek={[0, 6]} // no weekends
disabledDates={publicHolidays}
showYearControls // adds « » year paging
locale="en-GB"
/>You can always type into it. The text is parsed in the locale's own field order, so 03/04/2025
is 3 April in en-GB and 4 March in en-US — read from Intl, not from a hardcoded table.
Written months work too (Mar 20, 2024, 20 März 2024), which matters because the field displays
a month-name format by default.
Unparseable text is reverted, not cleared: a typo can't destroy a date you had already chosen. Emptying the field clears the value.
| Key | Does |
| -------- | ---------------------------------------------- |
| ↓ | Open the calendar and move focus into the grid |
| Enter | Commit what you typed |
| Escape | Discard what you typed / close the calendar |
DateRangePicker
Two date fields sharing one two-month calendar.
const [stay, setStay] = useState({ start: null, end: null });
<DateRangePicker
value={stay}
onChange={setStay}
startLabel="Check in"
endLabel="Check out"
min={new Date()}
/>;Which end a calendar click sets follows the focused field: click into "Check out" and pick a
day, and only the end moves. onChange fires twice per range — once with end: null while it is
half-picked, then again with both ends — so a controlled parent always sees the true state. A range
picked backwards is ordered rather than rejected.
TimePicker
A time field with stepped suggestions.
<TimePicker
aria-label="Start time"
value={startTime}
onChange={setStartTime}
minuteStep={15}
minTime="08:00"
maxTime="18:00"
/>The list is a convenience, not a constraint — typing 14:37 is accepted even on a 15-minute
list. Pass enforceBounds if minTime/maxTime should be hard limits. 2:30 pm, 14:30, 1430
and 930 all parse.
Changing the time never changes the date: the calendar day of the existing value is preserved.
DateTimePicker
One field for both, with a calendar and a time list in a single popup.
<DateTimePicker
aria-label="Starts at"
value={startsAt}
onChange={setStartsAt}
minuteStep={30}
defaultTime="09:00" // given to a date picked before any time was chosen
/>Choosing a day never disturbs the time and choosing a time never disturbs the day, so the two
halves can be set in either order. The popup does not close by itself — use Done, Escape, or
click away.
Calendar
The month grid the pickers are built from, usable standalone as an inline calendar.
// Single (default)
<Calendar value={date} onChange={setDate} />
// Multiple
<Calendar mode="multiple" value={dates} onChange={setDates} maxSelected={5} />
// Range, two months side by side
<Calendar mode="range" numberOfMonths={2} value={range} onChange={setRange} />| Key | Does |
| ----------------------------- | ------------------------------ |
| ← → | ± one day (mirrored under RTL) |
| ↑ ↓ | ± one week |
| Home / End | First / last day of the week |
| PageUp / PageDown | ± one month |
| Shift + PageUp/PageDown | ± one year |
| Enter / Space | Select the focused day |
Focus follows across month boundaries, paging the view when it needs to. Days blocked by
disabledDates/disabledDaysOfWeek stay focusable so a screen reader announces why they can't
be chosen; only min/max stop arrow movement.
Useful options: weekStartsOn (0 = Sunday, default 1 = Monday) · showOutsideDays ·
fixedWeeks (always six rows, so a popup doesn't resize while paging) · numberOfMonths ·
weekendDays · footer · today (inject a clock so tests don't depend on the day they run).
With @alphinex/forms
import {
DatePickerField,
DateRangePickerField,
DateTimePickerField,
TimePickerField,
} from "@alphinex/forms";
<Form form={form} onSubmit={save}>
<DatePickerField name="dueAt" label="Due date" min={new Date()} />
<TimePickerField name="reminderAt" label="Reminder" minuteStep={15} />
<DateTimePickerField name="startsAt" label="Starts at" />
<DateRangePickerField name="stay" label="Stay" />
</Form>;Each field stores a Date (or a { start, end } pair for the range) and accepts ISO strings in
defaultValues — a form hydrated from JSON works without any conversion on your side.
The engine
Every function below is exported and usable on its own, without rendering anything.
import { addMonths, toISODate, parseDateInput, buildMonthGrid } from "@alphinex/dates";Arithmetic — addDays addWeeks addMonths addYears addHours addMinutes ·
startOfDay endOfDay startOfWeek endOfWeek startOfMonth endOfMonth startOfYear
endOfYear · isSameDay isSameMonth isSameYear isToday isBeforeDay isAfterDay
isWithinDayRange · differenceInDays daysInMonth clampDate withTime · createDate
isValidDate
Formatting — formatDate formatTime formatDateTime formatMonthYear formatDayLabel ·
toISODate toISOTime toISODateTime · weekdayLabels weekdayFullLabels monthLabels
Parsing — parseDateInput parseTimeInput parseDateTimeInput parseISODate ·
coerceDate applyParsedTime getDateOrder
Grids — buildMonthGrid getMonthMatrix getYearRange moveFocusedDate ·
isDateDisabled isOutOfBounds
Two things worth knowing
toISODate reads the local calendar, not UTC. new Date(2024, 0, 5, 0, 30).toISOString()
gives 2024-01-04 anywhere east of Greenwich — the single most common date bug in web apps. Use
toISODate / toISODateTime to serialise, and coerceDate / parseISODate to read back.
addMonths clamps instead of overflowing. Native setMonth turns 31 January + 1 month into
2 March, which shows up as a picker appearing to skip February. Here it is 29 February (or the 28th
in a non-leap year). addYears does the same.
addMonths(new Date(2024, 0, 31), 1); // 29 Feb 2024
addYears(new Date(2024, 1, 29), 1); // 28 Feb 2025Custom calendars
buildMonthGrid is the same cell engine Calendar and @alphinex/calendar's month view use, so
a bespoke calendar agrees with the pickers by construction:
const weeks = buildMonthGrid({
month: new Date(),
weekStartsOn: 1,
range: { start, end },
constraints: { min, max, disabledDates },
fixedWeeks: true,
});
// weeks: DayCell[][] — each cell has date, key, dayOfMonth, isOutsideMonth,
// isToday, isSelected, isDisabled, isRangeStart, isRangeEnd, isInRange, isWeekendAccessibility
Calendaris a WAI-ARIAgridwith roving tabindex — one tab stop for the whole month.- The pickers follow the date-picker combobox pattern: the input is a
comboboxwitharia-haspopup="dialog", the popup is a non-modaldialogthat trapsTaband restores focus. - Trigger buttons are named after their field (
"Open calendar for Due date"), so two date fields on one page are distinguishable. - Every component is asserted against
axe-corein its own test suite.
License
UNLICENSED — internal to Alphinex Solutions.
