Esta página se tradujo automáticamente y puede contener errores. Leer el original en inglés

Elementos

DatePicker

Un date picker establece una fecha desde un Calendar que se abre desde el disparador de la app: una fecha de vencimiento, un cumpleaños, con range los días de un viaje, con timePrecision una fecha y una hora del día. Como Select, se abre como espera cada dispositivo:

el-date-picker.tsx
el-date-picker.tsxLive
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);
  • En los escritorios y en los navegadores de escritorio, un popup junto al disparador; al abrirlo, el teclado queda sobre los días.
  • En teléfonos y tabletas, una hoja en la parte inferior de la pantalla sobre un fondo (backdrop).

El selector se encarga del comportamiento: el valor (el de la app o el suyo propio), el estado de apertura, el calendario, la hora, los preajustes, el teclado y lo que oyen los lectores de pantalla. El aspecto es de la app: dibuja el disparador (con el valor, un marcador de posición, un icono) y da estilo al popup, a las partes del calendario, a los preajustes y al campo de hora.

use chrono::NaiveDate;
use nsh_base::{DateRangePreset, DateTime, PickedDate, date_picker, theme};
use nsh_core::prelude::*;

fn trip_dates() -> impl IntoElement {
    let trip = signal(DateTime::Range(None, None));
    let title = move || {
        trip.get()
            .format("%b %-d")
            .map(|s| s.to_string())
            .unwrap_or_else(|| "Pick dates".to_string())
    };
    let day = |m, d| NaiveDate::from_ymd_opt(2026, m, d).unwrap();
    date_picker()
        .range(true)
        .number_of_months(2)
        .value(move || trip.get())
        .on_change(move |next: &PickedDate| trip.set(next.value))
        .presets(vec![
            DateRangePreset::range("This week", day(10, 25), day(10, 31)),
            DateRangePreset::range("Next week", day(11, 1), day(11, 7)),
        ])
        .accessibility_label("Trip")
        .offset(px(4.0))
        .popup_style(|s| {
            s.p_3()
                .rounded_lg()
                .border_1()
                .border_color(move || theme().border)
                .bg(move || theme().popover)
                .shadow_md()
        })
        .trigger(
            view()
                .h_9()
                .w_56()
                .px_3()
                .justify_center()
                .rounded_md()
                .border_1()
                .border_color(move || theme().input)
                .child(text(title)),
        )
}

DatePicker viene de nsh/base, el crate nsh-base de Rust. En Rust, date_picker() crea uno con un setter por prop (.value(..), .range(..), .time_precision(..)) y un listener por evento (.on_change(..)). Su valor es un DateTime (DateTime::Single, DateTime::Range, a partir de fechas chrono o de fechas y horas); onChange devuelve un PickedDate, el valor con la precisión de hora del selector.

#Valor

El valor tiene las formas de los inputs de HTML: una fecha ISO, "2026-10-28", como la de <input type="date">; con range [start, end]; con timePrecision una fecha y hora, "2026-10-28T09:30" ("2026-10-28T09:30:15" con segundos), como la de <input type="datetime-local">; null es ninguno. Con value, el valor es de la app: el selector lo muestra e informa de cada cambio con onChange({ value }); la app establece value para seguirlo. Si se omite, el selector conserva su propio valor, que al principio viene de defaultValue.

  • Una sola fecha: pulsar un día establece el valor y cierra el popup.
  • Con range, la primera pulsación inicia el rango y la segunda lo termina, como en un calendario; el rango completo establece el valor y cierra el popup.
  • Con timePrecision (solo para una fecha), debajo del calendario se muestra un campo de hora y se informa de cada edición de la hora. En los escritorios, elegir un día mantiene el popup abierto para la hora e informa de la fecha con su hora; volver a pulsar el día seleccionado cierra el popup. En teléfonos y tabletas, tocar un día lo elige con su hora y cierra la hoja, como hace una sola fecha; la hoja se vuelve a abrir para la hora. Un día elegido antes de fijar una hora recibe defaultTime (por defecto 00:00). hourCycle es cómo cuenta las horas el campo.

cleanable añade un botón de borrado mientras haya un valor: al pulsarlo se borra (onChange({ value: null })). Va después del disparador, no dentro de él, de modo que pulsarlo nunca abre el popup; la app lo coloca y lo rellena (clearButton), como el botón de borrado de Select.

#Preajustes

Los presets son botones junto al calendario que establecen el valor: “Hoy”, “Últimos 7 días”, “Mañana a las 9”. Cada uno es { label, value }, value una fecha, un rango o fechas y horas, conservado tal como se da; al pulsarlo se establece y se cierra el popup. contentClass organiza la columna de preajustes y el calendario con la hora (una fila, flex-row); presetsClass da estilo a la columna y presetClass a cada botón.

#El calendario

Todas las props de Calendar salvo su valor se pasan a las del selector: numberOfMonths, firstDayOfWeek, disabledMatcher, yearRange, previousAccessibilityLabel, nextAccessibilityLabel, las partes (de headerClass a itemStyle) y renderItem. calendarClass y calendarStyle dan estilo al propio calendario. Sus vistas y teclas son las de un calendario.

La fila de la hora (timeClass, timeStyle) contiene timeLabel, las palabras de la app para ella (“Hora”), al principio, y el campo de hora (timeFieldClass, timeFieldStyle, timeAccessibilityLabel) al final: una fila con sus elementos centrados y separados (flex-row items-center justify-between, como el de gpui-component). Los segmentos del campo son los de un TimeField (segmentClass, separatorClass, renderSegment).

#Props

Prop Rust Tipo Por defecto Descripción
value .value(..) DatePickerValue ninguno El valor, que pertenece a la app.
defaultValue .default_value(..) DatePickerValue null El valor inicial, sin value.
range .range(..) boolean false Elegir un rango de fechas.
open, defaultOpen .open(..), .default_open(..) boolean false El estado de apertura, el de la app o el propio del selector.
disabled .disabled(..) boolean false Permanece cerrado y no admite pulsaciones ni foco de teclado.
accessibilityLabel .accessibility_label(..) string ninguno Cómo lo llaman los lectores de pantalla.
dateFormat .date_format(..) string "%Y/%m/%d" y la hora Cómo oyen el valor los lectores de pantalla, en el formato de chrono.
timePrecision .time_precision(..) "minute" | "second" ninguno Edita también la hora del día, hasta esta precisión.
hourCycle .hour_cycle(..) "h23" | "h12" "h23" Cómo cuenta las horas el campo de hora.
defaultTime .default_time(..) string "00:00" La hora que recibe una fecha antes de que el usuario fije una.
cleanable .cleanable(..) boolean false Un botón de borrado mientras haya un valor.
presets .presets(..) DateRangePreset[] [] Botones del popup que establecen el valor.
numberOfMonths, firstDayOfWeek, disabledMatcher, yearRange como en Calendar como en Calendar como en Calendar Los meses, semanas, fechas deshabilitadas y años del calendario.
previousAccessibilityLabel, nextAccessibilityLabel como en Calendar string ninguno Lo que dicen los lectores de pantalla para Anterior y Siguiente del calendario.
placement, align, offset, margin .placement(..), … como en Select "bottom", "start", 0, 0 Escritorios: dónde se coloca el popup respecto al disparador.
popupClass, popupStyle .popup_class(..), .popup_style(..) string, StyleProp<ViewStyle> ninguno El popup (escritorios) o la hoja (pantallas táctiles).
backdropClass, backdropStyle .backdrop_class(..), .backdrop_style(..) string, StyleProp<ViewStyle> ninguno El fondo (backdrop) detrás de la hoja.
contentClass, contentStyle .content_class(..), .content_style(..) string, StyleProp<ViewStyle> ninguno El contenido del popup: los preajustes junto al calendario y la hora.
presetsClass, presetsStyle .presets_class(..), .presets_style(..) string, StyleProp<ViewStyle> ninguno La columna de preajustes.
presetClass, presetStyle .preset_class(..), .preset_style(..) string, StyleProp<TextStyle> ninguno El botón de cada preajuste.
calendarClass, calendarStyle .calendar_class(..), .calendar_style(..) string, StyleProp<ViewStyle> ninguno El calendario.
headerClass … itemStyle como en Calendar como en Calendar ninguno El encabezado, el cuerpo, los meses, las filas y los elementos del calendario.
renderItem .render_item(..) (item: CalendarEntry) => JSX.Element ninguno Cada elemento del calendario, dibujado por la app según lo que es; sus estados son las clases de los elementos, como en Calendar.
timeClass, timeStyle .time_class(..), .time_style(..) string, StyleProp<ViewStyle> ninguno La fila de la hora: su etiqueta al principio, el campo al final.
timeLabel .time_label(..) JSX.Element ninguno Lo que muestra la fila de la hora antes del campo.
timeFieldClass, timeFieldStyle .time_field_class(..), .time_field_style(..) string, StyleProp<ViewStyle> ninguno El campo de hora.
timeAccessibilityLabel .time_accessibility_label(..) string ninguno Cómo llaman los lectores de pantalla al campo de hora.
segmentClass, segmentStyle, separatorClass, separatorStyle, renderSegment como en TimeField como en TimeField ninguno Los segmentos del campo de hora y :.
clearButtonClass, clearButtonStyle .clear_button_class(..), .clear_button_style(..) string, StyleProp<ViewStyle> ninguno El botón de borrado (cleanable).
clearButton .clear_button(..) JSX.Element ninguno El contenido del botón de borrado.
clearButtonAccessibilityLabel .clear_button_accessibility_label(..) string ninguno Cómo llaman los lectores de pantalla al botón de borrado.
trigger .trigger(..) JSX.Element ninguno Lo que abre el popup al pulsarlo; el popup se coloca respecto a él.
class, style métodos de estilo string, StyleProp<ViewStyle> ninguno La vista que rodea al disparador.

#Eventos

Evento Rust Descripción
onChange .on_change(|value| ..) El valor cambió (un día, el final de un rango, un preajuste, la hora, el botón de borrado): { value }. Establece value para seguirlo.
onOpenChange .on_open_change(|e| ..) El popup se abrió o se cerró, con el motivo ({ open, reason }). Establece open para seguirlo.
onOpen, onClose .on_open(..), .on_close(..) El popup se muestra, o ya no está.
onCancel .on_cancel(..) El usuario cerró el popup sin elegir (Escape o atrás, una pulsación fuera o en el fondo).

#Partes

Parte Dónde Estilizado con
El disparador la vista del selector el elemento propio de la app (trigger)
El botón de borrado la vista del selector, después del disparador (cleanable, mientras haya un valor) clearButtonClass, clearButtonStyle; su contenido clearButton
El popup o la hoja sobre todo lo demás, junto al disparador (escritorios) o en la parte inferior (pantallas táctiles) popupClass, popupStyle
El fondo (backdrop) detrás de la hoja (pantallas táctiles) backdropClass, backdropStyle
El contenido el popup contentClass, contentStyle
Los preajustes el contenido, primero (presets) presetsClass, presetsStyle; cada uno presetClass, presetStyle
El calendario el contenido calendarClass, calendarStyle, y las partes de Calendar
La fila de la hora debajo del calendario (timePrecision) timeClass, timeStyle; su etiqueta timeLabel
El campo de hora la fila de la hora timeFieldClass, timeFieldStyle, y las partes de TimeField

#Teclado

Tecla Acción
Return (en el disparador) Abre el popup, o lo cierra.
Delete, Backspace (en el disparador) Borra el valor mientras el popup está cerrado.
Escape Cierra el popup sin elegir.

En el popup, las teclas del calendario se mueven entre los días y eligen uno (véase Teclado) y las del campo de hora editan la hora (véase Teclado).

#Accesibilidad

  • Para los lectores de pantalla, el disparador es un combobox: su etiqueta es accessibilityLabel, su valor es la fecha tal como la escribe dateFormat (por defecto 2026/10/28, con la hora en timePrecision), e indica si el popup está expandido. disabled lo atenúa.
  • Los días del calendario son botones nombrados por sus fechas ISO (véase Accesibilidad); los preajustes son botones nombrados por sus etiquetas; el campo de hora se nombra con timeAccessibilityLabel.
  • El botón de borrado es un botón nombrado con clearButtonAccessibilityLabel.

Consulta Accesibilidad para ver etiquetas, roles y anuncios en general.