Elementi
Calendar
Un calendario permette di scegliere una data, o con range un intervallo di date, tra mesi di giorni disposti in settimane: una scadenza in un modulo, le notti di un soggiorno, i giorni di un report. Mostra un mese o più mesi affiancati, scorre i mesi con Previous e Next e passa a una vista dei mesi dell’anno o a una pagina di anni per spostarsi lontano. È il calendario nel popup di DatePicker, usato da solo nella pagina.
import { type JSX, mount, signal, Text, View, windowSize } from "nsh";
import { type CalendarEntry, DatePicker, type DatePickerValue, type DateRangePreset, type HourCycle, type OpenChangeReason, type TimeFieldSegmentEntry } from "nsh/base";
import { button, desktop, Icon, Log, Panel, Segmented, size, Split, 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;
/**
* On a touch screen the calendar spans the sheet (the window less the sheet's padding), its columns sharing it: the
* header's titles split what Previous and Next leave, so the month and the year meet in the middle, and each month's
* title of two is over its month.
*/
const toggleWidth = () => Math.floor((windowSize().width - 2 * 10) / 2 - cell);
/** 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 };
/** The calendar: in the popup with a pointer; across the sheet on a touch screen, as the time's row under it. */
const calendar = desktop ? { padding: 2 } : {};
/** 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 } : {}),
});
/** 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().muted }}>
{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>
);
}
/**
* The calendar's items (`itemClass`): pressed at 70%; `group`, so the
* drawing inside follows the item's states.
*/
const itemClass = "group active:opacity-70";
/**
* An item of the calendar, drawn once from what it is, its states the
* item's classes: Previous and Next as arrows (faint while they take no
* press), the month and year toggles (or each month's title, with `months`
* over one) by name, the open view's toggle in the accent, the weekdays'
* initials, and the days: today on the theme's quiet `accent` fill
* (gpui-component's), the selected date and a range's ends filled, the days
* of a complete range on a band from end to end (`data-range-*`), the other
* months' days and the disabled ones faint, the keyboard's day ringed.
*/
function Item(props: { item: CalendarEntry; months?: number }) {
const kind = props.item.kind;
if (kind === "previous" || kind === "next") {
return (
<View class="group-disabled:opacity-30" style={{ width: cell, height: cell, alignItems: "center", justifyContent: "center" }}>
<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.months ?? 1) > 1;
return (
<Text
class="text-foreground group-selected:text-brand"
style={{
width: !desktop ? toggleWidth() : 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",
}}
>
{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={{ minWidth: 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; the shown one
// filled.
return (
<View
class="rounded-lg group-selected:bg-brand-fill"
style={{ minWidth: (7 * cell) / (kind === "month" ? 3 : 5), height: cell + 6, alignItems: "center", justifyContent: "center" }}
>
<Text class="text-foreground group-selected:text-on-brand" style={{ fontSize: controlText }}>
{kind === "month" ? MONTHS[props.item.value - 1].slice(0, 3) : String(props.item.value)}
</Text>
</View>
);
}
// A day, as wide as its column (the calendar's columns share its width). The band spans a range's middle days and
// reaches from its ends' middles; a range of one day has none. The circle fills for the selected date and a
// range's ends (not its middle days), else for today.
const own = props.item.outside ? "text-faint" : "text-foreground";
return (
<View style={{ minWidth: cell, height: cell, alignItems: "center", justifyContent: "center" }}>
<View
class={
"absolute inset-y-0.5 left-0 right-0 hidden bg-brand-tint group-data-range-middle:flex group-data-range-start:flex " +
"group-data-range-start:left-1/2 group-data-range-end:flex group-data-range-end:right-1/2 group-data-range-start:group-data-range-end:hidden"
}
/>
<View
class={
`${props.item.today ? "bg-accent" : ""} border-[1.5px] border-transparent group-focus:border-info ` +
"group-selected:bg-brand-fill group-data-range-middle:bg-transparent"
}
style={{ width: cell - 4, height: cell - 4, borderRadius: (cell - 4) / 2, alignItems: "center", justifyContent: "center" }}
>
<Text
class={
`${own} ${props.item.today ? "font-semibold" : "font-normal"} group-selected:font-semibold group-selected:text-on-brand ` +
`group-data-range-middle:${own} group-disabled:text-faint group-disabled:opacity-50`
}
style={{ fontSize: controlText, fontVariant: ["tabular-nums"] }}
>
{String(props.item.value)}
</Text>
</View>
</View>
);
}
/**
* A segment of the time: filled while it has the keys (`focus:` on the
* segment, `segmentClass`), its digits or AM / PM in the color on the fill
* (`group-focus:`), so moving between the segments draws nothing.
*/
const segmentClass = "group rounded active:opacity-70 focus:bg-brand-fill";
function Segment(props: { segment: TimeFieldSegmentEntry }) {
return (
<Text class="text-foreground group-focus:text-on-brand" style={{ paddingHorizontal: 3, fontSize: controlText, fontVariant: ["tabular-nums"] }}>
{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={itemClass}
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={<Icon name="close" size={14} tint={ui().muted} />}
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={itemClass}
renderItem={(item) => <Item item={item} months={wide() ? 2 : 1} />}
trigger={<Trigger title={stayTitle()} placeholder="Check-in – check-out" open={stayOpen()} />}
/>
</Field>
<Field label="Meeting" detail={desktop ? "A day, then its time; the day again closes the calendar" : "A tap picks the day; open it again for the time"}>
<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={itemClass}
renderItem={(item) => <Item item={item} />}
timeStyle={time()}
timeLabel={<Text style={{ fontSize: controlText, color: ui().muted }}>Time</Text>}
timeAccessibilityLabel="Meeting time"
timeFieldClass={timeField}
segmentClass={segmentClass}
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);Il calendario gestisce il comportamento: il valore (dell’app o proprio), il mese mostrato, le viste, i giorni che non possono essere scelti, la tastiera e ciò che sentono gli screen reader. L’aspetto spetta all’app: questa stila l’intestazione, il corpo, ogni mese, ogni riga e ogni elemento, e disegna il contenuto degli elementi (i nomi dei mesi e dei giorni della settimana, il segno di oggi, la fascia dell’intervallo). La griglia riempie la larghezza che le viene data, come fanno le colonne di una tabella: i mesi se la dividono e gli elementi di una riga si dividono la riga in parti uguali, quindi un calendario nel foglio di un telefono occupa tutto lo schermo, mentre uno in un popup dimensionato sul contenuto mantiene la larghezza dei propri elementi (flex-none in itemClass o monthClass mantiene un elemento o un mese alla propria larghezza).
use nsh_base::{CalendarEntry, CalendarItemKind, Date, Matcher, calendar};
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")
.item_class("rounded-md selected:bg-primary selected:text-primary-foreground disabled:opacity-50")
.render_item(|item: &CalendarEntry| {
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()
.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="group 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 data-range-middle:rounded-none data-range-middle:bg-accent data-range-middle:text-accent-foreground data-outside:text-muted-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 proviene da nsh/base, il crate nsh-base in Rust. In Rust, calendar() ne crea uno con un setter per ogni prop (.value(..), .range(..), .number_of_months(..)) e un listener per ogni evento (.on_change(..)). Il suo valore è un Date (Date::Single, Date::Range, da un chrono::NaiveDate o da una coppia di essi), mentre i giorni che disabilita sono un Matcher.
#Value
Il valore è una data ISO, "2026-10-28", come quella di un <input type="date"> HTML; con range è [start, end], e null indica nessuna data. Con value il valore appartiene all’app: il calendario lo mostra come selezionato e segnala una scelta con onChange({ value }); l’app imposta value per seguirla. Se omesso, il calendario mantiene un valore proprio, inizialmente da defaultValue.
onChange segnala solo valori completi, come il Selected di gpui-base: un giorno premuto, o la fine di un intervallo.
- Senza
range, la pressione di un giorno lo sceglie. - Con
range, la prima pressione avvia l’intervallo e la seconda lo termina, con i giorni compresi nell’intervallo (ciascunoselected, estremi inclusi, vedi Items). Una seconda pressione su un giorno precedente fa ripartire l’intervallo da quel giorno, così come una pressione a intervallo completo. Un intervallo parziale viene mostrato finché non è completo e solo allora viene segnalato.
Il calendario si apre sul mese del valore (l’inizio di un intervallo), altrimenti su quello di oggi.
#Months and views
numberOfMonths mostra quel numero di mesi affiancati, a partire da quello mostrato; Previous e Next li scorrono un mese alla volta. firstDayOfWeek è il giorno da cui iniziano le settimane, da 0 domenica (il valore predefinito) a 6 sabato.
L’intestazione contiene Previous, due toggle e Next:
- La vista dei giorni mostra i giorni dei mesi, ogni mese come una colonna con la riga dei giorni della settimana e le sue settimane; i giorni dei mesi precedente e successivo completano le settimane (
outside,data-outside). - Il toggle del mese (
monthToggle) apre la vista dei mesi: i dodici mesi, in righe di tre. Sceglierne uno lo mostra nella vista dei giorni. - Il toggle dell’anno (
yearToggle) apre la vista degli anni: una pagina di 20 anni, in righe di cinque, con Previous e Next che scorrono le pagine. Sceglierne uno mostra quell’anno nella vista dei giorni.yearRangeindica gli anni che le pagine attraversano,[start, end)(predefinito: da 50 anni prima di quello corrente a 50 dopo).
Con più mesi, l’intestazione mostra il titolo di ciascun mese al posto dei toggle (un elemento monthToggle che non accetta pressioni), come quello di gpui-base.
#Disabled dates
disabledMatcher indica i giorni che l’utente non può scegliere, il Matcher di gpui-base, in una di quattro forme:
| Forma | Disabilita |
|---|---|
[0, 6] | Quei giorni della settimana, 0 domenica fino a 6 sabato. |
{ before, after } | I giorni prima di before e dopo after (uno dei due può essere omesso). |
{ from, to } | I giorni da from a to, inclusi (un estremo omesso è aperto). |
(date) => boolean | I giorni per i quali la funzione restituisce true, data la data ISO di ogni giorno mostrato. |
La funzione viene interrogata una volta per ogni giorno mostrato, nel turno JS successivo, e non di nuovo per un giorno a cui ha già risposto: scorrere i mesi interroga solo sui giorni che portano con sé, quindi la sua risposta deve essere una funzione pura della data. Passare un’altra funzione interroga di nuovo. Un giorno disabilitato è disabled:: non accetta pressioni, e Return e Space non lo scelgono.
#Items
Ogni destinazione di pressione e ogni etichetta del calendario è un elemento: Previous, i toggle, Next, i giorni della settimana, i giorni, i mesi e gli anni. Ciascuno è disegnato in una parte stilata da itemClass e itemStyle, le cui varianti seguono il suo stato:
| Variante | L’elemento |
|---|---|
selected: | È selezionato: la data, ogni giorno di un intervallo completo (estremi inclusi, come il range calendar di React Aria), l’inizio di un intervallo parziale, il mese o l’anno mostrato, il toggle della vista aperta. Gli screen reader lo annunciano come selezionato. |
disabled: | Non accetta pressioni: una data disabilitata, un giorno della settimana, il titolo di un mese, Previous e Next nella vista dei mesi o agli estremi degli anni. |
focus: | Ha la tastiera. |
data-range-start:, data-range-end: | È il primo o l’ultimo giorno di un intervallo completo (un intervallo di un giorno è entrambi). |
data-range-middle: | È un giorno interno a un intervallo completo, non un estremo. |
data-today: | È oggi. |
data-outside: | È un giorno del mese precedente o successivo, che completa le sue settimane. |
Le varianti data-* sono quelle di Tailwind (data-range-start: corrisponde a [data-range-start]), come quelle con cui il calendario di shadcn stila i suoi giorni; con group in itemClass, il contenuto le segue con group-selected:, group-data-range-middle:, e così via. selected: indica ogni giorno di un intervallo, quindi gli estremi di un intervallo prendono il proprio aspetto sopra di esso (le varianti successive prevalgono: data-* viene dopo selected:), e i suoi giorni centrali il proprio:
<Calendar
range
itemClass="group rounded-md selected:bg-primary selected:text-primary-foreground data-range-middle:rounded-none data-range-middle:bg-accent data-range-middle:text-accent-foreground data-range-start:rounded-r-none data-range-end:rounded-l-none data-range-start:data-range-end:rounded-md"
/>
renderItem disegna il contenuto di un elemento a partire da ciò che è, un CalendarEntry, mai dal suo stato: il disegno viene fatto una sola volta e mantenuto mentre l’elemento è selezionato, ha il focus o fa parte di un intervallo, quindi i tasti, una scelta e il focus non ridisegnano nulla, e uno scorrimento dei mesi disegna una sola volta i giorni che porta con sé (un giorno mantiene il proprio disegno in base al mese e alla data: con più mesi, i mesi ancora mostrati mantengono il loro).
| Campo | Significato |
|---|---|
kind | "previous", "monthToggle", "yearToggle", "next", "weekday", "day", "month" o "year". |
value | Il suo numero: il giorno del mese, il mese (da 1 a 12), l’anno, il giorno della settimana (0 domenica fino a 6); 0 per Previous e Next. |
date | La data ISO di un giorno. |
year, month | L’anno e il mese a cui appartiene: quelli di un giorno (il mese le cui settimane lo mostrano), quelli del toggle o del titolo di un mese, quelli di un mese della vista dei mesi; l’anno proprio di un toggle dell’anno e di un anno. 0 per Previous, Next e i giorni della settimana (e month per il toggle dell’anno e per gli anni). |
today | Il giorno è oggi. |
outside | Il giorno appartiene al mese precedente o successivo. |
Senza renderItem, un elemento mostra il proprio numero, e Previous e Next mostrano ‹ e ›, come quelli di gpui-base; anche un giorno della settimana mostra il proprio numero, da 0 a 6, poiché i nomi dei mesi e dei giorni della settimana sono parole dell’app, nella lingua dell’app.
#Props
| Prop | Rust | Tipo | Predefinito | Descrizione |
|---|---|---|---|---|
value | .value(..) | CalendarValue | nessuno | Il valore, di proprietà dell’app. |
defaultValue | .default_value(..) | CalendarValue | null | Il valore iniziale, senza value. |
range | .range(..) | boolean | false | Sceglie un intervallo: la prima pressione lo avvia, la seconda lo termina. |
numberOfMonths | .number_of_months(..) | number | 1 | Quanti mesi vengono mostrati affiancati. |
firstDayOfWeek | .first_day_of_week(..) | number | 0 | Il giorno da cui iniziano le settimane, da 0 domenica a 6 sabato. |
disabledMatcher | .disabled_matcher(..) | DateMatcher | nessuno | Le date che non possono essere scelte. |
yearRange | .year_range(..) | [number, number] | 50 anni in più o in meno | Gli anni che la vista degli anni attraversa, [start, end). |
previousAccessibilityLabel | .previous_accessibility_label(..) | string | nessuno | Ciò che gli screen reader dicono per Previous. |
nextAccessibilityLabel | .next_accessibility_label(..) | string | nessuno | Ciò che gli screen reader dicono per Next. |
headerClass, headerStyle | .header_class(..), .header_style(..) | string, StyleProp<ViewStyle> | nessuno | L’intestazione: Previous, i toggle (o i titoli dei mesi), Next. |
bodyClass, bodyStyle | .body_class(..), .body_style(..) | string, StyleProp<ViewStyle> | nessuno | Il corpo: i mesi affiancati, o le righe della vista dei mesi o degli anni. |
monthClass, monthStyle | .month_class(..), .month_style(..) | string, StyleProp<ViewStyle> | nessuno | Ogni mese della vista dei giorni. |
rowClass, rowStyle | .row_class(..), .row_style(..) | string, StyleProp<ViewStyle> | nessuno | Ogni riga: i giorni della settimana, una settimana, una riga di mesi o di anni. |
itemClass, itemStyle | .item_class(..), .item_style(..) | string, StyleProp<TextStyle> | nessuno | Ogni elemento: selected:, disabled:, focus:; di un giorno data-range-start:, data-range-end:, data-range-middle:, data-today:, data-outside: (Items). |
renderItem | .render_item(..) | (item: CalendarEntry) => JSX.Element | nessuno | Il contenuto di un elemento, disegnato dall’app a partire da ciò che l’elemento è; altrimenti il suo numero. |
class, style | metodi di stile | string, StyleProp<ViewStyle> | nessuno | Il calendario: l’intestazione sopra il corpo. |
#Events
| Evento | Rust | Descrizione |
|---|---|---|
onChange | .on_change(|value| ..) | L’utente ha scelto un valore completo: { value }, un giorno o un intervallo. Imposta value per seguirlo. Un value passato dall’app non viene mai segnalato. |
#Parts
| Parte | Dove | Stilata con |
|---|---|---|
| Il calendario | la radice | class, style |
| L’intestazione | il calendario, per prima | headerClass, headerStyle |
| Il corpo | il calendario, dopo l’intestazione | bodyClass, bodyStyle |
| Un mese | il corpo (vista dei giorni) | monthClass, monthStyle |
| Una riga | un mese, o il corpo (viste dei mesi e degli anni) | rowClass, rowStyle |
| Un elemento | l’intestazione e le righe | itemClass, itemStyle, renderItem (group-selected:, group-data-range-middle:, … all’interno, sotto group in itemClass) |
#Keyboard
I giorni sono una sola tappa di Tab: questo si posiziona sul giorno selezionato, altrimenti su oggi, altrimenti sul primo giorno mostrato. Poi:
| Tasto | Azione |
|---|---|
| Sinistra / Destra | Il giorno prima o dopo. |
| Su / Giù | Lo stesso giorno una settimana prima o dopo. |
| Home / End | Il primo o l’ultimo giorno della settimana. |
| Page Up / Page Down | Lo stesso giorno un mese prima o dopo. |
| Shift+Page Up / Shift+Page Down | Lo stesso giorno un anno prima o dopo. |
| Return, Space | Sceglie il giorno. |
In un layout da destra a sinistra le settimane scorrono da destra, quindi Sinistra è il giorno dopo e Destra il giorno prima; Su e Giù mantengono le settimane. Superare i mesi mostrati li fa scorrere. Previous, i toggle, Next, i mesi e gli anni accettano pressioni.
I giorni di gpui-base accettano solo pressioni; i tasti sono quelli di NSH, secondo il date picker dialog di WAI-ARIA.
#Accessibility
- Ogni giorno è un pulsante identificato dalla sua data ISO, selezionato quando è la data o un giorno dell’intervallo (estremi inclusi, come il range calendar di React Aria), attenuato quando è disabilitato.
- Previous e Next sono pulsanti identificati da
previousAccessibilityLabelenextAccessibilityLabel; i toggle, i mesi e gli anni sono pulsanti identificati da ciò che mostrano.
Vedi Accessibility per etichette, ruoli e annunci in generale.