Elementos
Calendar
Un calendario elige una fecha, o con range un rango de fechas, entre meses de días dispuestos en semanas: una fecha límite en un formulario, las noches de una estancia, los días de un informe. Muestra un mes o varios lado a lado, pasa los meses con Previous y Next, y cambia a una vista de los meses del año o a una página de años para ir lejos. Es el calendario del popup de DatePicker, solo en la página.
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);El calendario es dueño del comportamiento: el valor (el de la app, o el suyo propio), el mes mostrado, las vistas, los días que no se pueden elegir, el teclado y lo que oyen los lectores de pantalla. El aspecto es de la app: da estilo al encabezado, al cuerpo, a cada mes, a cada fila y a cada elemento, y dibuja el contenido de los elementos (los nombres de los meses y de los días de la semana, la marca de hoy, la banda del rango). La cuadrícula ocupa el ancho que se le da, como las columnas de una tabla: los meses se lo reparten y los elementos de una fila se reparten la fila por igual, de modo que un calendario en la hoja de un teléfono ocupa toda la pantalla y uno en un popup dimensionado a su contenido conserva los anchos de sus elementos (flex-none en itemClass o monthClass mantiene un elemento o un mes en su propio ancho).
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 viene de nsh/base, el crate nsh-base de Rust. En Rust, calendar() crea uno con un setter por prop (.value(..), .range(..), .number_of_months(..)) y un listener por evento (.on_change(..)). Su valor es un Date (Date::Single, Date::Range, a partir de un chrono::NaiveDate o de un par de ellos), y los días que deshabilita son un Matcher.
#Valor
El valor es una fecha ISO, "2026-10-28", como la de un <input type="date"> de HTML; con range es [start, end], y null significa sin fecha. Con value, la app es la dueña del valor: el calendario lo muestra como seleccionado e informa de una elección con onChange({ value }); la app asigna value para seguirla. Si se omite, el calendario conserva su propio valor, que al principio viene de defaultValue.
onChange informa solo de valores completos, como el Selected de gpui-base: un día pulsado, o el final de un rango.
- Sin
range, pulsar un día lo elige. - Con
range, la primera pulsación inicia el rango y la segunda lo termina, y los días entre ambas quedan en el rango (cada unoselected, con los extremos incluidos, véase Elementos). Una segunda pulsación en un día anterior reinicia el rango desde él, igual que una pulsación cuando el rango ya está completo. Un rango a medias se muestra hasta que se completa y solo entonces se informa.
El calendario se abre en el mes del valor (el inicio de un rango) y, si no hay valor, en el de hoy.
#Meses y vistas
numberOfMonths muestra ese número de meses lado a lado, a partir del mes mostrado; Previous y Next los pasan de uno en uno. firstDayOfWeek es el día en que empiezan las semanas, de 0 domingo (el valor por defecto) a 6 sábado.
El encabezado contiene Previous, dos interruptores y Next:
- La vista de días muestra los días de los meses, cada mes como una columna con su fila de días de la semana y sus semanas; los días de los meses anterior y posterior completan las semanas (
outside,data-outside). - El interruptor de mes (
monthToggle) abre la vista de meses: los doce meses, en filas de tres. Al elegir uno, se muestra en la vista de días. - El interruptor de año (
yearToggle) abre la vista de años: una página de 20 años, en filas de cinco, con Previous y Next para pasar de página. Al elegir uno, se muestra ese año en la vista de días.yearRangeson los años que recorren las páginas,[start, end)(por defecto: de 50 años antes de este a 50 después).
Con varios meses, el encabezado muestra el título de cada mes en lugar de los interruptores (un elemento monthToggle que no admite pulsación), como el de gpui-base.
#Fechas deshabilitadas
disabledMatcher son los días que el usuario no puede elegir, el Matcher de gpui-base, en una de cuatro formas:
| Forma | Deshabilita |
|---|---|
[0, 6] |
Esos días de la semana, 0 domingo a 6 sábado. |
{ before, after } |
Los días anteriores a before y posteriores a after (se puede omitir cualquiera de los dos). |
{ from, to } |
Los días desde from hasta to, ambos incluidos (si se omite uno, queda abierto). |
(date) => boolean |
Los días para los que la función devuelve true, dada la fecha ISO de cada día mostrado. |
La función se consulta una vez por día mostrado, en el siguiente turno de JS, y no se vuelve a consultar por un día que ya respondió: al pasar los meses solo se pregunta por los días que traen, por lo que su respuesta debe ser una función pura de la fecha. Pasar otra función vuelve a consultar. Un día deshabilitado es disabled:: no admite pulsación, y Return y Space no lo eligen.
#Elementos
Cada objetivo de pulsación y cada etiqueta del calendario es un elemento: Previous, los interruptores, Next, los días de la semana, los días, los meses y los años. Cada uno se dibuja en una parte con estilo de itemClass y itemStyle, cuyas variantes siguen su estado:
| Variante | El elemento |
|---|---|
selected: |
Está seleccionado: la fecha, cada día de un rango completo (con los extremos incluidos, como el range calendar de React Aria), el inicio de un rango a medias, el mes o año mostrado, el interruptor de la vista abierta. Los lectores de pantalla lo oyen como seleccionado. |
disabled: |
No admite pulsación: una fecha deshabilitada, un día de la semana, el título de un mes, Previous y Next en la vista de meses o en los extremos de los años. |
focus: |
Tiene el teclado. |
data-range-start:, data-range-end: |
Es el primer o el último día de un rango completo (un rango de un solo día es ambos). |
data-range-middle: |
Es un día dentro de un rango completo, sin ser un extremo. |
data-today: |
Es hoy. |
data-outside: |
Es un día del mes anterior o posterior, que completa sus semanas. |
Las variantes de data-* son las de Tailwind (data-range-start: coincide con [data-range-start]), como en los días del calendario de shadcn; con group en itemClass, el contenido las sigue con group-selected:, group-data-range-middle:, y así sucesivamente. selected: significa cada día de un rango, de modo que los extremos de un rango toman su aspecto por encima de él (las variantes posteriores ganan: data-* va después de selected:), y sus días intermedios el suyo:
<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 dibuja el contenido de un elemento a partir de lo que es, un CalendarEntry, nunca a partir de su estado: el dibujo se hace una vez y se conserva mientras el elemento está seleccionado, enfocado o incluido en un rango, de modo que las teclas, una elección y el foco no dibujan nada de nuevo, y al pasar los meses se dibujan una sola vez los días que traen (un día conserva su dibujo según su mes y su fecha: con varios meses, los meses que siguen mostrándose conservan los suyos).
| Campo | Significado |
|---|---|
kind |
"previous", "monthToggle", "yearToggle", "next", "weekday", "day", "month" o "year". |
value |
Su número: el día del mes, el mes (1 a 12), el año, el día de la semana (0 domingo a 6); 0 para Previous y Next. |
date |
La fecha ISO de un día. |
year, month |
El año y el mes a los que pertenece: los de un día (el mes cuyas semanas lo muestran), los del interruptor o título de un mes, los de un mes de la vista de meses; el año propio de un interruptor de año y de un año. 0 para Previous, Next y los días de la semana (y month para el interruptor de año y los años). |
today |
El día es hoy. |
outside |
El día es del mes anterior o posterior. |
Sin renderItem, un elemento muestra su número, y Previous y Next muestran ‹ y ›, como los de gpui-base; un día de la semana también muestra su número, de 0 a 6, ya que los nombres de los meses y de los días de la semana son palabras de la app, en el idioma de la app.
#Props
| Prop | Rust | Tipo | Por defecto | Descripción |
|---|---|---|---|---|
value |
.value(..) |
CalendarValue |
ninguno | El valor, del que es dueña la app. |
defaultValue |
.default_value(..) |
CalendarValue |
null |
El valor inicial, sin value. |
range |
.range(..) |
boolean |
false |
Elegir un rango: la primera pulsación lo inicia, la segunda lo termina. |
numberOfMonths |
.number_of_months(..) |
number |
1 |
Cuántos meses se muestran lado a lado. |
firstDayOfWeek |
.first_day_of_week(..) |
number |
0 |
El día en que empiezan las semanas, de 0 domingo a 6 sábado. |
disabledMatcher |
.disabled_matcher(..) |
DateMatcher |
ninguno | Las fechas que no se pueden elegir. |
yearRange |
.year_range(..) |
[number, number] |
50 años a cada lado | Los años que recorre la vista de años, [start, end). |
previousAccessibilityLabel |
.previous_accessibility_label(..) |
string |
ninguno | Lo que dicen los lectores de pantalla para Previous. |
nextAccessibilityLabel |
.next_accessibility_label(..) |
string |
ninguno | Lo que dicen los lectores de pantalla para Next. |
headerClass, headerStyle |
.header_class(..), .header_style(..) |
string, StyleProp<ViewStyle> |
ninguno | El encabezado: Previous, los interruptores (o los títulos de los meses), Next. |
bodyClass, bodyStyle |
.body_class(..), .body_style(..) |
string, StyleProp<ViewStyle> |
ninguno | El cuerpo: los meses lado a lado, o las filas de la vista de meses o de años. |
monthClass, monthStyle |
.month_class(..), .month_style(..) |
string, StyleProp<ViewStyle> |
ninguno | Cada mes de la vista de días. |
rowClass, rowStyle |
.row_class(..), .row_style(..) |
string, StyleProp<ViewStyle> |
ninguno | Cada fila: los días de la semana, una semana, una fila de meses o de años. |
itemClass, itemStyle |
.item_class(..), .item_style(..) |
string, StyleProp<TextStyle> |
ninguno | Cada elemento: selected:, disabled:, focus:; los data-range-start:, data-range-end:, data-range-middle:, data-today:, data-outside: de un día (Elementos). |
renderItem |
.render_item(..) |
(item: CalendarEntry) => JSX.Element |
ninguno | El contenido de un elemento, dibujado por la app a partir de lo que es el elemento; su número en caso contrario. |
class, style |
métodos de estilo | string, StyleProp<ViewStyle> |
ninguno | El calendario: el encabezado sobre el cuerpo. |
#Eventos
| Evento | Rust | Descripción |
|---|---|---|
onChange |
.on_change(|value| ..) |
El usuario eligió un valor completo: { value }, un día o un rango. Asigna value para seguirlo. Nunca se informa de un value que pasa la app. |
#Partes
| Parte | Dónde | Estilo con |
|---|---|---|
| El calendario | la raíz | class, style |
| El encabezado | el calendario, primero | headerClass, headerStyle |
| El cuerpo | el calendario, después del encabezado | bodyClass, bodyStyle |
| Un mes | el cuerpo (vista de días) | monthClass, monthStyle |
| Una fila | un mes, o el cuerpo (vistas de meses y de años) | rowClass, rowStyle |
| Un elemento | el encabezado y las filas | itemClass, itemStyle, renderItem (group-selected:, group-data-range-middle:, … dentro, bajo group en itemClass) |
#Teclado
Los días son una única parada de Tab: aterriza en el día seleccionado, si no en hoy, si no en el primer día mostrado. Después:
| Tecla | Acción |
|---|---|
| Izquierda / Derecha | El día anterior o posterior. |
| Arriba / Abajo | El mismo día una semana antes o después. |
| Home / End | El primer o el último día de la semana. |
| Page Up / Page Down | El mismo día un mes antes o después. |
| Shift+Page Up / Shift+Page Down | El mismo día un año antes o después. |
| Return, Space | Elige el día. |
En un diseño de derecha a izquierda las semanas corren desde la derecha, así que Izquierda es el día posterior y Derecha el anterior; Arriba y Abajo mantienen las semanas. Al pasar de los meses mostrados, estos se desplazan. Previous, los interruptores, Next, los meses y los años admiten pulsaciones.
Los días de gpui-base solo admiten pulsaciones; las teclas son las de NSH, según el diálogo de selector de fecha de WAI-ARIA.
#Accesibilidad
- Cada día es un botón nombrado por su fecha ISO, seleccionado mientras es la fecha o un día del rango (con los extremos incluidos, como el range calendar de React Aria), y atenuado mientras está deshabilitado.
- Previous y Next son botones nombrados por
previousAccessibilityLabelynextAccessibilityLabel; los interruptores, los meses y los años son botones nombrados por lo que muestran.
Consulta Accesibilidad para ver las etiquetas, los roles y los anuncios en general.