Elements
Calendar
A calendar picks a date, or with range a range of dates, from months of days laid out in weeks: a due date in a
form, the nights of a stay, the days of a report. It shows one month or several side by side, turns the months with
Previous and Next, and switches to a view of the year’s months or a page of years to go far. It is the calendar in
DatePicker’s popup, on its own in the page.
import { type JSX, mount, signal, Text, token, View } from "nsh";
import { type CalendarItemState, DatePicker, type DatePickerValue, type DateRangePreset, type HourCycle, type OpenChangeReason, type TimeFieldSegmentState } from "nsh/base";
import { button, desktop, Log, Panel, Segmented, size, Split, tint, ui, wide } from "./palette";
// Three date pickers in a card: a due date (weekends disabled, presets
// beside the calendar, a clear button), a stay picked as a range over two
// months in a wide window, and a meeting's date and time with the hours
// on the clock the inspector picks. The trigger is the shared button look
// with the value and a chevron that turns while the calendar is open; the
// calendar is a popup at the trigger, a sheet on a touch screen. Every
// item is drawn by `renderItem` (month names, weekday initials, today on a
// quiet fill, the selected days filled, a range's band) and every segment
// of the time by `renderSegment`; the time's label starts its row and the
// field, a well as the number inputs', ends it.
const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
const WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
/** A day's ISO date (`"2026-10-28"`), in local time as the calendar's days are. */
const iso = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
const addDays = (d: Date, days: number) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + days);
/** An ISO date's month and day: `Oct 28`. */
const monthDay = (value: string) => `${MONTHS[Number(value.slice(5, 7)) - 1].slice(0, 3)} ${Number(value.slice(8, 10))}`;
/** A time `"HH:MM"` on the picked clock: `14:30`, or `2:30 PM`. */
function clock(time: string, cycle: HourCycle) {
if (cycle === "h23") return time;
const hour = Number(time.slice(0, 2));
return `${hour % 12 === 0 ? 12 : hour % 12}:${time.slice(3, 5)} ${hour < 12 ? "AM" : "PM"}`;
}
/** The text of a control: 13 with a pointer, the touch label size otherwise. */
const controlText = desktop ? 13 : size.label;
/** A day's cell: seven across the popup with a pointer, across a phone's sheet otherwise. */
const cell = desktop ? 32 : 42;
/** The gap between the months of a calendar of two. */
const monthGap = 16;
/** A popup at the trigger with a pointer, a sheet at the bottom on a touch screen. */
const surface = () =>
desktop
? { ...ui().card, borderRadius: 12, padding: 8 }
: { ...ui().card, borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingHorizontal: 10, paddingTop: 14, paddingBottom: 30 };
const backdrop = { backgroundColor: "rgba(0,0,0,0.45)" };
/** The popup's layout: presets in a column beside the calendar with a pointer, a row of chips above it on a phone. */
const content = desktop ? { flexDirection: "row" as const, gap: 10 } : { gap: 12 };
const presets = () =>
desktop
? { gap: 2, minWidth: 104, paddingRight: 10, borderRightWidth: 1, borderColor: ui().border }
: { flexDirection: "row" as const, flexWrap: "wrap" as const, gap: 6 };
const preset = () =>
desktop
? { height: 28, paddingHorizontal: 8, borderRadius: 6, justifyContent: "center" as const, fontSize: 13, color: ui().text }
: { height: 34, paddingHorizontal: 12, borderRadius: 17, borderWidth: 1, borderColor: ui().border, justifyContent: "center" as const, fontSize: 14, color: ui().text };
const calendar = desktop ? { padding: 2 } : { alignSelf: "center" as const };
/** The time's row under a hairline, its label at the start and the field at the end, on the calendar's edges. */
const time = () => ({
gap: 10,
marginTop: 8,
paddingTop: 8,
borderTopWidth: 1,
borderColor: ui().border,
...(desktop ? { paddingHorizontal: 2 } : { alignSelf: "center" as const, width: 7 * cell }),
});
/** The time's field: a recessed well as the number inputs', with their hover and the keyboard's ring while it edits. */
const timeField = [
desktop ? "h-8 rounded-lg px-2" : "h-11 rounded-xl px-3",
"items-center border border-input bg-field",
"enabled:hover:border-faint focus-within:outline-2 focus-within:outline-info",
].join(" ");
/** The trigger: the shared button's look, the value at the start and a chevron in a lane as wide as it. */
function Trigger(props: { title?: string; placeholder: string; open: boolean; clearable?: boolean }) {
const look = button("secondary", {
style: () => ({
flexDirection: "row",
justifyContent: "space-between",
gap: 8,
...(desktop ? { width: 260 } : {}),
...(props.open ? { borderColor: ui().faint } : {}),
}),
});
return (
<View {...look}>
<Text style={{ numberOfLines: 1, flex: 1, fontSize: controlText, color: props.title ? ui().text : ui().faint }}>
{props.title ?? props.placeholder}
</Text>
{/* With a clear button over the end (`cleanable`), an empty lane as wide as the button in place of the chevron. */}
<View
class="transition-transform duration-150 motion-reduce:transition-none"
style={{
width: props.clearable ? 20 : 10,
height: 20,
alignItems: "center",
justifyContent: "center",
opacity: props.clearable ? 0 : 1,
transform: [{ rotate: props.open ? "270deg" : "90deg" }],
}}
>
<Text style={{ fontSize: 18, color: ui().muted }}>›</Text>
</View>
</View>
);
}
/**
* An item of the calendar: Previous and Next as arrows, the month and year toggles (or each month's
* title) by name, the weekdays' initials, and the days: today on the theme's quiet `accent` fill
* (gpui-component's), the selected ones filled,
* the days of a complete range on a band from end to end (`range` says where it starts and ends), the
* other months' days and the disabled ones faint.
*/
function Item(props: { item: CalendarItemState; range?: DatePickerValue }) {
const kind = props.item.kind;
if (kind === "previous" || kind === "next") {
return (
<View style={{ width: cell, height: cell, alignItems: "center", justifyContent: "center", opacity: props.item.disabled ? 0.3 : 1 }}>
<Text style={{ fontSize: 20, color: ui().muted }}>{kind === "previous" ? "‹" : "›"}</Text>
</View>
);
}
if (kind === "monthToggle" || kind === "yearToggle") {
// A month's title in a calendar of several takes no press: the month and its year, over the month.
const title = kind === "monthToggle" && props.item.disabled;
return (
<Text
style={{
width: title ? 6 * cell + monthGap / 2 : kind === "monthToggle" ? 3 * cell : 2 * cell,
paddingHorizontal: 3,
textAlign: title ? "center" : kind === "monthToggle" ? "right" : "left",
fontSize: size.label,
fontWeight: "600",
color: props.item.active ? ui().accent : ui().text,
}}
>
{title ? `${MONTHS[props.item.value - 1]} ${props.item.year}` : kind === "monthToggle" ? MONTHS[props.item.value - 1] : String(props.item.value)}
</Text>
);
}
if (kind === "weekday") {
return <Text style={{ width: cell, paddingVertical: 4, textAlign: "center", fontSize: 11, color: ui().faint }}>{WEEKDAYS[props.item.value]}</Text>;
}
if (kind === "month" || kind === "year") {
// The month view's months (rows of 3) and the year view's years (rows of 5), as wide as a month.
return (
<View
style={{
width: (7 * cell) / (kind === "month" ? 3 : 5),
height: cell + 6,
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
backgroundColor: props.item.active ? ui().accentFill.backgroundColor : "transparent",
}}
>
<Text style={{ fontSize: controlText, color: props.item.active ? ui().onAccent : ui().text }}>
{kind === "month" ? MONTHS[props.item.value - 1].slice(0, 3) : String(props.item.value)}
</Text>
</View>
);
}
// A day. The band reaches from the start's middle to the end's.
const band = () => {
const r = props.range;
if (!props.item.inRange || !Array.isArray(r) || r[0] === r[1]) return null;
return { left: props.item.date === r[0] ? cell / 2 : 0, right: props.item.date === r[1] ? cell / 2 : 0 };
};
return (
<View style={{ width: cell, height: cell, alignItems: "center", justifyContent: "center" }}>
<View style={{ position: "absolute", top: 2, bottom: 2, left: band()?.left ?? 0, right: band()?.right ?? 0, backgroundColor: band() ? tint(ui().accent, 0.14) : "transparent" }} />
<View
style={{
width: cell - 4,
height: cell - 4,
borderRadius: (cell - 4) / 2,
alignItems: "center",
justifyContent: "center",
borderWidth: 1.5,
// The keyboard's day ringed.
borderColor: props.item.focused ? ui().info : "transparent",
backgroundColor: props.item.active ? ui().accentFill.backgroundColor : props.item.today ? token("accent") : "transparent",
}}
>
<Text
style={{
fontSize: controlText,
fontVariant: ["tabular-nums"],
fontWeight: props.item.active || props.item.today ? "600" : "400",
color: props.item.active ? ui().onAccent : props.item.muted ? ui().faint : ui().text,
opacity: props.item.disabled ? 0.5 : 1,
}}
>
{String(props.item.value)}
</Text>
</View>
</View>
);
}
/** A segment of the time: its digits, or AM / PM, filled while it has the keys. */
function Segment(props: { segment: TimeFieldSegmentState }) {
return (
<Text
style={{
paddingHorizontal: 3,
borderRadius: 4,
fontSize: controlText,
fontVariant: ["tabular-nums"],
color: props.segment.selected ? ui().onAccent : ui().text,
backgroundColor: props.segment.selected ? ui().accentFill.backgroundColor : "transparent",
}}
>
{props.segment.label}
</Text>
);
}
/** A label over its control, a quieter line under the label. */
function Field(props: { label: string; detail: string; children: JSX.Element }) {
return (
<View style={{ gap: 8 }}>
<View style={{ gap: 2 }}>
<Text style={{ fontSize: size.label, fontWeight: "500", color: ui().text }}>{props.label}</Text>
<Text style={{ fontSize: size.detail, color: ui().muted }}>{props.detail}</Text>
</View>
{props.children}
</View>
);
}
function App() {
const due = signal<string | null>(null);
const stay = signal<DatePickerValue>(null);
const meeting = signal<string | null>(null);
const cycle = signal<HourCycle>("h12");
const dueOpen = signal(false);
const stayOpen = signal(false);
const meetingOpen = signal(false);
const events = signal<string[]>([]);
const log = (line: string) => events.set([line, ...events()].slice(0, 8));
const opened = (open: (next: boolean) => void) => ({ open: next }: { open: boolean; reason: OpenChangeReason }) => open(next);
// The presets from today.
const today = new Date();
const monday = addDays(today, ((8 - today.getDay()) % 7) || 7);
const dues: DateRangePreset[] = [
{ label: "Today", value: iso(today) },
{ label: "Tomorrow", value: iso(addDays(today, 1)) },
{ label: "Next Monday", value: iso(monday) },
];
const stays: DateRangePreset[] = [
{ label: "This week", value: [iso(today), iso(addDays(today, (7 - today.getDay()) % 7))] },
{ label: "Next 7 nights", value: [iso(today), iso(addDays(today, 7))] },
];
const stayTitle = () => {
const r = stay();
return Array.isArray(r) && r[0] && r[1] ? `${monthDay(r[0])} – ${monthDay(r[1])}, ${r[1].slice(0, 4)}` : undefined;
};
const meetingTitle = () => {
const v = meeting();
return v ? `${monthDay(v)}, ${v.slice(0, 4)} · ${clock(v.slice(11, 16), cycle())}` : undefined;
};
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>date-picker</Text>
<Text style={{ color: ui().muted }}>
{desktop ? "Arrows move between days, Return picks; the trigger takes Delete to clear" : "Tap a day; the meeting's time takes digits"}
</Text>
<Split
gap={14}
inspector={
<>
<Panel title="value">
<Text style={{ fontSize: 13, color: ui().text }}>{`due ${JSON.stringify(due())}`}</Text>
<Text style={{ fontSize: 13, color: ui().text }}>{`stay ${JSON.stringify(stay())}`}</Text>
<Text style={{ fontSize: 13, color: ui().text }}>{`meeting ${JSON.stringify(meeting())}`}</Text>
</Panel>
<Segmented label="hourCycle" options={["h12", "h23"] as const} value={cycle()} onChange={(v) => cycle.set(v)} />
<Log title="onChange" lines={events()} minHeight={120} />
</>
}
>
<View style={{ ...ui().card, borderRadius: 16, padding: 16, gap: 20 }}>
<Field label="Due" detail="Weekdays only, presets beside the calendar, a clear button">
<DatePicker
value={due()}
onChange={({ value }) => {
due.set(typeof value === "string" ? value : null);
log(`Due → ${JSON.stringify(value)}`);
}}
onOpenChange={opened((next) => dueOpen.set(next))}
accessibilityLabel="Due"
disabledMatcher={[0, 6]}
presets={dues}
offset={4}
margin={8}
popupStyle={surface()}
backdropStyle={backdrop}
contentStyle={content}
presetsStyle={presets()}
presetStyle={preset()}
presetClass="active:opacity-70"
calendarStyle={calendar}
headerStyle={{ marginBottom: 4 }}
itemClass="active:opacity-70"
renderItem={(item) => <Item item={item} />}
style={desktop ? { width: 260 } : undefined}
cleanable
clearButtonAccessibilityLabel="Clear the due date"
clearButtonClass="absolute items-center justify-center rounded-full enabled:hover:opacity-70"
clearButtonStyle={{ top: (size.control - 20) / 2, right: 10, width: 20, height: 20 }}
clearButton={<Text style={{ fontSize: 12, color: ui().muted }}>✕</Text>}
trigger={<Trigger title={due() ? `${monthDay(due()!)}, ${due()!.slice(0, 4)}` : undefined} placeholder="No due date" open={dueOpen()} clearable={due() !== null} />}
/>
</Field>
<Field label="Stay" detail={wide() ? "A range over two months: the first press starts it" : "A range: the first press starts it, the second ends it"}>
<DatePicker
range
value={stay()}
onChange={({ value }) => {
stay.set(value);
log(`Stay → ${JSON.stringify(value)}`);
}}
onOpenChange={opened((next) => stayOpen.set(next))}
accessibilityLabel="Stay"
numberOfMonths={wide() ? 2 : 1}
firstDayOfWeek={1}
presets={stays}
offset={4}
margin={8}
popupStyle={surface()}
backdropStyle={backdrop}
contentStyle={content}
presetsStyle={presets()}
presetStyle={preset()}
presetClass="active:opacity-70"
calendarStyle={calendar}
headerStyle={{ marginBottom: 4 }}
bodyStyle={{ gap: monthGap }}
itemClass="active:opacity-70"
renderItem={(item) => <Item item={item} range={stay()} />}
trigger={<Trigger title={stayTitle()} placeholder="Check-in – check-out" open={stayOpen()} />}
/>
</Field>
<Field label="Meeting" detail="A day, then its time; the day again closes the calendar">
<DatePicker
value={meeting()}
onChange={({ value }) => {
meeting.set(typeof value === "string" ? value : null);
log(`Meeting → ${JSON.stringify(value)}`);
}}
onOpenChange={opened((next) => meetingOpen.set(next))}
accessibilityLabel="Meeting"
timePrecision="minute"
hourCycle={cycle()}
defaultTime="09:00"
offset={4}
margin={8}
popupStyle={surface()}
backdropStyle={backdrop}
calendarStyle={calendar}
headerStyle={{ marginBottom: 4 }}
itemClass="active:opacity-70"
renderItem={(item) => <Item item={item} />}
timeStyle={time()}
timeLabel={<Text style={{ fontSize: controlText, color: ui().muted }}>Time</Text>}
timeAccessibilityLabel="Meeting time"
timeFieldClass={timeField}
segmentClass="rounded active:opacity-70"
separatorStyle={{ fontSize: controlText, color: ui().faint }}
renderSegment={(segment) => <Segment segment={segment} />}
trigger={<Trigger title={meetingTitle()} placeholder="Not scheduled" open={meetingOpen()} />}
/>
</Field>
</View>
</Split>
</View>
);
}
mount(App);The calendar owns the behavior: the value (the app’s, or its own), the shown month, the views, the days that cannot be picked, the keyboard and what screen readers hear. The look is the app’s: it styles the header, the body, each month, each row and each item, and draws the items’ content (the months’ and weekdays’ names, today’s mark, the range’s band).
use nsh_base::{CalendarItemKind, CalendarItemState, Date, Matcher, calendar, theme};
use nsh_core::prelude::*;
const MONTHS: [&str; 12] = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
];
const WEEKDAYS: [&str; 7] = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
fn due_date() -> impl IntoElement {
let due = signal(Date::default());
calendar()
.value(move || due.get())
.on_change(move |next: &Date| due.set(*next))
.disabled_matcher(Matcher::DayOfWeek(vec![0, 6]))
.previous_accessibility_label("Previous month")
.next_accessibility_label("Next month")
.render_item(|item: &CalendarItemState| {
let label = match item.kind {
CalendarItemKind::Previous => "‹".to_string(),
CalendarItemKind::Next => "›".to_string(),
CalendarItemKind::Weekday => WEEKDAYS[item.value as usize].to_string(),
CalendarItemKind::MonthToggle | CalendarItemKind::Month => {
MONTHS[item.value as usize - 1].to_string()
}
_ => item.value.to_string(),
};
view()
.w_8()
.h_8()
.items_center()
.justify_center()
.rounded_md()
.when(item.active, |v| v.bg(move || theme().primary))
.child(text(label).text_sm())
})
}import { mount, signal, Text } from "nsh";
import { Calendar, type CalendarValue } from "nsh/base";
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
function App() {
const due = signal<CalendarValue>(null);
return (
<Calendar
value={due()}
onChange={({ value }) => due.set(value)}
disabledMatcher={[0, 6]}
previousAccessibilityLabel="Previous month"
nextAccessibilityLabel="Next month"
class="w-72 gap-2 rounded-lg border border-border p-3"
headerClass="flex-row items-center justify-between"
rowClass="flex-row justify-between"
itemClass="h-8 min-w-8 items-center justify-center rounded-md text-sm focus:ring-2 focus:ring-ring selected:bg-primary selected:text-primary-foreground disabled:opacity-50"
renderItem={(item) => (
<Text class={item.today ? "font-semibold" : ""}>
{item.kind === "weekday"
? WEEKDAYS[item.value]
: item.kind === "month" || item.kind === "monthToggle"
? MONTHS[item.value - 1]
: item.kind === "previous"
? "‹"
: item.kind === "next"
? "›"
: String(item.value)}
</Text>
)}
/>
);
}
mount(App);Calendar comes from nsh/base, the nsh-base crate in Rust. In Rust, calendar() makes
one with a setter per prop (.value(..), .range(..), .number_of_months(..)) and a listener per event
(.on_change(..)). Its value is a Date (Date::Single, Date::Range, from a chrono::NaiveDate or a pair of
them), the days it disables a Matcher.
#Value
The value is an ISO date, "2026-10-28", as an HTML <input type="date">’s; with range it is [start, end], and
null is no date. With value, the app owns it: the calendar shows it as selected and reports a pick with
onChange({ value }); the app sets value to follow it. Left out, the calendar keeps its own value, from
defaultValue at first.
onChange reports complete values only, as gpui-base’s Selected: a pressed day, or a range’s end.
- Without
range, a press on a day picks it. - With
range, the first press starts the range and the second ends it, the days between them in the range (inRangeon each, its ends included). A second press on an earlier day starts the range over from it, as does a press once the range is complete. A half range shows until it is complete and only then is reported.
The calendar opens on the month of the value (a range’s start), else today’s.
#Months and views
numberOfMonths shows that many months side by side, from the shown one on; Previous and Next turn them one month at
a time. firstDayOfWeek is the day the weeks start on, 0 Sunday (the default) to 6 Saturday.
The header holds Previous, two toggles and Next:
- The day view shows the months’ days, each month a column of its weekdays’ row and its weeks; the days of the
months before and after fill the weeks and are
muted. - The month toggle (
monthToggle) opens the month view: the twelve months, in rows of three. Picking one shows it in the day view. - The year toggle (
yearToggle) opens the year view: a page of 20 years, in rows of five, Previous and Next turning the page. Picking one shows that year in the day view.yearRangeis the years the pages run through,[start, end)(default: 50 years before this one to 50 after).
With several months, the header shows each month’s title instead of the toggles (a monthToggle item that takes no
press), as gpui-base’s.
#Disabled dates
disabledMatcher is the days the user cannot pick, gpui-base’s Matcher, in one of four forms:
| Form | Disables |
|---|---|
[0, 6] | Those days of the week, 0 Sunday to 6 Saturday. |
{ before, after } | The days before before and after after (either left out). |
{ from, to } | The days from from to to, inclusive (either left out is open). |
(date) => boolean | The days for which the function returns true, given each shown day’s ISO date. |
The function is asked once per shown day, in the next JS turn, so its answer is a pure function of the date. A
disabled day is disabled and muted: it takes no press, and Return and Space do not pick it.
#Items
Every press target and label of the calendar is an item: Previous, the toggles, Next, the weekdays, the days, the
months and the years. Each is drawn in a part styled by itemClass and itemStyle, whose variants follow its state:
selected: while active (the date or a range’s end, the shown month or year, the open view’s toggle), disabled:
while it takes no press, focus: with the keyboard on it.
renderItem draws an item’s content from its CalendarItemState:
| Field | Meaning |
|---|---|
kind | "previous", "monthToggle", "yearToggle", "next", "weekday", "day", "month" or "year". |
value | Its number: the day of the month, the month (1 to 12), the year, the day of the week (0 Sunday to 6); 0 for Previous and Next. |
date | A day’s ISO date. |
year, month | The year and month it belongs to, else the shown ones. |
active | It is selected. |
inRange | A day inside the complete range, its ends included. |
muted | A weekday, a day of the month before or after, a disabled day. |
disabled | It takes no press. |
today | The day is today. |
focused | The keyboard is on the day. |
Without renderItem, an item shows its number, and Previous and Next show ‹ and ›, as gpui-base’s; a weekday
shows its number too, 0 to 6, since the names of the months and weekdays are the app’s words in the app’s
language.
#Props
| Prop | Rust | Type | Default | Description |
|---|---|---|---|---|
value | .value(..) | CalendarValue | none | The value, owned by the app. |
defaultValue | .default_value(..) | CalendarValue | null | The value at first, without value. |
range | .range(..) | boolean | false | Pick a range: the first press starts it, the second ends it. |
numberOfMonths | .number_of_months(..) | number | 1 | How many months show side by side. |
firstDayOfWeek | .first_day_of_week(..) | number | 0 | The day the weeks start on, 0 Sunday to 6 Saturday. |
disabledMatcher | .disabled_matcher(..) | DateMatcher | none | The dates that cannot be picked. |
yearRange | .year_range(..) | [number, number] | 50 years either way | The years the year view pages through, [start, end). |
previousAccessibilityLabel | .previous_accessibility_label(..) | string | none | What screen readers say for Previous. |
nextAccessibilityLabel | .next_accessibility_label(..) | string | none | What screen readers say for Next. |
headerClass, headerStyle | .header_class(..), .header_style(..) | string, StyleProp<ViewStyle> | none | The header: Previous, the toggles (or the months’ titles), Next. |
bodyClass, bodyStyle | .body_class(..), .body_style(..) | string, StyleProp<ViewStyle> | none | The body: the months side by side, or the month or year view’s rows. |
monthClass, monthStyle | .month_class(..), .month_style(..) | string, StyleProp<ViewStyle> | none | Each month of the day view. |
rowClass, rowStyle | .row_class(..), .row_style(..) | string, StyleProp<ViewStyle> | none | Each row: the weekdays, a week, a row of months or years. |
itemClass, itemStyle | .item_class(..), .item_style(..) | string, StyleProp<TextStyle> | none | Each item: selected:, disabled:, focus:. |
renderItem | .render_item(..) | (item: CalendarItemState) => JSX.Element | none | An item’s content, drawn by the app; its number otherwise. |
class, style | style methods | string, StyleProp<ViewStyle> | none | The calendar: the header above the body. |
#Events
| Event | Rust | Description |
|---|---|---|
onChange | .on_change(|value| ..) | The user picked a complete value: { value }, a day or a range. Set value to follow it. A value the app passes is never reported. |
#Parts
| Part | Where | Styled with |
|---|---|---|
| The calendar | the root | class, style |
| The header | the calendar, first | headerClass, headerStyle |
| The body | the calendar, after the header | bodyClass, bodyStyle |
| A month | the body (day view) | monthClass, monthStyle |
| A row | a month, or the body (month and year views) | rowClass, rowStyle |
| An item | the header and the rows | itemClass, itemStyle, renderItem |
#Keyboard
The days are one stop of Tab: it lands on the selected day, else today, else the first shown day. Then:
| Key | Action |
|---|---|
| Left / Right | The day before or after. |
| Up / Down | The same day a week before or after. |
| Home / End | The first or last day of the week. |
| Page Up / Page Down | The same day a month before or after. |
| Shift+Page Up / Shift+Page Down | The same day a year before or after. |
| Return, Space | Picks the day. |
Moving past the shown months turns them. Previous, the toggles, Next, the months and the years take presses.
gpui-base’s days take presses only; the keys are NSH’s, after the WAI-ARIA date picker dialog.
#Accessibility
- Each day is a button named by its ISO date, selected while it is the date or a range’s end, dimmed while disabled.
- Previous and Next are buttons named by
previousAccessibilityLabelandnextAccessibilityLabel; the toggles, months and years are buttons named by what they show.
See Accessibility for labels, roles and announcements in general.