Diese Seite wurde maschinell übersetzt und kann Fehler enthalten. Das englische Original lesen

Elemente

Calendar

Ein Kalender wählt ein Datum aus, oder mit range einen Datumsbereich, aus Monaten mit in Wochen angeordneten Tagen: ein Fälligkeitsdatum in einem Formular, die Nächte eines Aufenthalts, die Tage eines Berichts. Er zeigt einen Monat oder mehrere nebeneinander, blättert die Monate mit Zurück und Weiter und wechselt zu einer Ansicht der Monate des Jahres oder zu einer Seite mit Jahren, um weit zu springen. Es ist der Kalender im Popup von DatePicker, hier für sich allein auf der Seite.

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);

Der Kalender besitzt das Verhalten: den Wert (den der App oder seinen eigenen), den angezeigten Monat, die Ansichten, die nicht wählbaren Tage, die Tastatur und das, was Screenreader vorlesen. Das Aussehen gehört der App: Sie gestaltet Header, Body, jeden Monat, jede Zeile und jedes Element und zeichnet den Inhalt der Elemente (die Namen der Monate und Wochentage, die Markierung für heute, das Band des Bereichs). Das Raster füllt die ihm zugewiesene Breite, wie die Spalten einer Tabelle: Die Monate teilen sie sich, und die Elemente einer Zeile teilen sich die Zeile zu gleichen Teilen. So überspannt ein Kalender im Sheet eines Smartphones den Bildschirm, und einer in einem am Inhalt ausgerichteten Popup behält die Breiten seiner Elemente (flex-none in itemClass oder monthClass lässt ein Element oder einen Monat bei seiner eigenen Breite).

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())
        })
}

Calendar stammt aus nsh/base, dem nsh-base-Crate in Rust. In Rust erzeugt calendar() einen Kalender mit einem Setter pro Prop (.value(..), .range(..), .number_of_months(..)) und einem Listener pro Event (.on_change(..)). Sein Wert ist ein Date (Date::Single, Date::Range, aus einem chrono::NaiveDate oder einem Paar davon), die Tage, die er deaktiviert, ein Matcher.

#Wert

Der Wert ist ein ISO-Datum, "2026-10-28", wie bei einem HTML-<input type="date">; mit range ist er [start, end], und null bedeutet kein Datum. Mit value gehört er der App: Der Kalender zeigt ihn als ausgewählt an und meldet eine Auswahl mit onChange({ value }); die App setzt value, um ihr zu folgen. Wird er weggelassen, behält der Kalender seinen eigenen Wert, zunächst aus defaultValue.

onChange meldet nur vollständige Werte, wie gpui-base mit seinem Selected: ein angetippter Tag oder das Ende eines Bereichs.

  • Ohne range wählt ein Tipp auf einen Tag diesen aus.
  • Mit range beginnt der erste Tipp den Bereich und der zweite beendet ihn; die Tage dazwischen gehören zum Bereich (jeweils selected, die Enden eingeschlossen, siehe Elemente). Ein zweiter Tipp auf einen früheren Tag beginnt den Bereich bei diesem Tag neu, ebenso ein Tipp, nachdem der Bereich vollständig ist. Ein halber Bereich wird angezeigt, bis er vollständig ist, und erst dann gemeldet.

Der Kalender öffnet sich auf dem Monat des Werts (bei einem Bereich dem Monat des Starts), sonst auf dem heutigen.

#Monate und Ansichten

numberOfMonths zeigt so viele Monate nebeneinander an, ab dem angezeigten; Zurück und Weiter blättern sie jeweils um einen Monat. firstDayOfWeek ist der Tag, an dem die Wochen beginnen, 0 Sonntag (der Standard) bis 6 Samstag.

Der Header enthält Zurück, zwei Umschalter und Weiter:

  • Die Tagesansicht zeigt die Tage der Monate, jeder Monat als Spalte aus der Zeile seiner Wochentage und seinen Wochen; die Tage der Monate davor und danach füllen die Wochen (outside, data-outside).
  • Der Monats-Umschalter (monthToggle) öffnet die Monatsansicht: die zwölf Monate in Zeilen zu je drei. Die Auswahl eines Monats zeigt ihn in der Tagesansicht.
  • Der Jahres-Umschalter (yearToggle) öffnet die Jahresansicht: eine Seite mit 20 Jahren in Zeilen zu je fünf, wobei Zurück und Weiter die Seite blättern. Die Auswahl eines Jahres zeigt dieses Jahr in der Tagesansicht. yearRange sind die Jahre, durch die die Seiten laufen, [start, end) (Standard: 50 Jahre vor diesem bis 50 danach).

Bei mehreren Monaten zeigt der Header statt der Umschalter den Titel jedes Monats (ein monthToggle-Element, das keinen Tipp annimmt), wie bei gpui-base.

#Deaktivierte Daten

disabledMatcher sind die Tage, die der Nutzer nicht auswählen kann, gpui-base mit seinem Matcher, in einer von vier Formen:

Form Deaktiviert
[0, 6] Diese Wochentage, 0 Sonntag bis 6 Samstag.
{ before, after } Die Tage vor before und nach after (jeweils optional).
{ from, to } Die Tage von from bis to einschließlich (ein weggelassener Wert ist offen).
(date) => boolean Die Tage, für die die Funktion true zurückgibt, wobei ihr das ISO-Datum jedes angezeigten Tages übergeben wird.

Die Funktion wird einmal pro angezeigtem Tag im nächsten JS-Turn aufgerufen und nicht erneut für einen Tag, den sie bereits beantwortet hat: Das Blättern der Monate fragt nur nach den Tagen, die sie neu hinzubringen, daher muss ihre Antwort eine reine Funktion des Datums sein. Die Übergabe einer anderen Funktion fragt erneut. Ein deaktivierter Tag ist disabled:: Er nimmt keinen Tipp an, und Return und Leertaste wählen ihn nicht aus.

#Elemente

Jedes Tipp-Ziel und jedes Label des Kalenders ist ein Element: Zurück, die Umschalter, Weiter, die Wochentage, die Tage, die Monate und die Jahre. Jedes wird in einem Part gezeichnet, der mit itemClass und itemStyle gestaltet wird und dessen Varianten seinem Zustand folgen:

Variante Das Element
selected: Ist ausgewählt: das Datum, jeder Tag eines vollständigen Bereichs (die Enden eingeschlossen, wie beim Range Calendar von React Aria), der Start eines halben Bereichs, der angezeigte Monat oder das angezeigte Jahr, der Umschalter der geöffneten Ansicht. Screenreader lesen es als ausgewählt vor.
disabled: Nimmt keinen Tipp an: ein deaktiviertes Datum, ein Wochentag, der Titel eines Monats, Zurück und Weiter in der Monatsansicht oder an den Enden der Jahre.
focus: Hat die Tastatur.
data-range-start:, data-range-end: Ist der erste bzw. letzte Tag eines vollständigen Bereichs (ein Bereich aus einem Tag ist beides).
data-range-middle: Ist ein Tag innerhalb eines vollständigen Bereichs, aber kein Ende.
data-today: Ist heute.
data-outside: Ist ein Tag des Monats davor oder danach, der die Wochen füllt.

Die data-*-Varianten stammen von Tailwind (data-range-start: entspricht [data-range-start]), so wie der Kalender von shadcn seine Tage gestaltet; mit group in itemClass folgt der Inhalt ihnen mit group-selected:, group-data-range-middle: und so weiter. selected: steht für jeden Tag eines Bereichs, sodass die Enden eines Bereichs ihr Aussehen darüber erhalten (spätere Varianten gewinnen: data-* kommt nach selected:), und seine mittleren Tage ihres:

<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 zeichnet den Inhalt eines Elements aus dem, was es ist, einem CalendarEntry, niemals aus seinem Zustand: Das Zeichnen geschieht einmal und bleibt erhalten, während das Element ausgewählt, fokussiert oder in einen Bereich aufgenommen wird. Die Tasten, eine Auswahl und der Fokus zeichnen also nichts neu, und ein Blättern der Monate zeichnet die Tage, die es bringt, je einmal (ein Tag behält seine Zeichnung anhand seines Monats und Datums: Bei mehreren Monaten behalten die weiterhin angezeigten Monate ihre).

Feld Bedeutung
kind "previous", "monthToggle", "yearToggle", "next", "weekday", "day", "month" oder "year".
value Seine Nummer: der Tag des Monats, der Monat (1 bis 12), das Jahr, der Wochentag (0 Sonntag bis 6); 0 für Zurück und Weiter.
date Das ISO-Datum eines Tages.
year, month Das Jahr und der Monat, zu denen es gehört: bei einem Tag der Monat, dessen Wochen ihn zeigen; bei einem Monats-Umschalter oder -Titel sein Monat; bei einem Monat der Monatsansicht sein eigener; bei einem Jahres-Umschalter und einem Jahr das eigene Jahr. 0 für Zurück, Weiter und die Wochentage (und month für den Jahres-Umschalter und die Jahre).
today Der Tag ist heute.
outside Der Tag gehört zum Monat davor oder danach.

Ohne renderItem zeigt ein Element seine Nummer, und Zurück und Weiter zeigen ‹ und ›, wie bei gpui-base; auch ein Wochentag zeigt seine Nummer, 0 bis 6, da die Namen der Monate und Wochentage die Wörter der App in der Sprache der App sind.

#Props

Prop Rust Typ Standard Beschreibung
value .value(..) CalendarValue keiner Der Wert, der der App gehört.
defaultValue .default_value(..) CalendarValue null Der Anfangswert, ohne value.
range .range(..) boolean false Einen Bereich wählen: Der erste Tipp beginnt ihn, der zweite beendet ihn.
numberOfMonths .number_of_months(..) number 1 Wie viele Monate nebeneinander angezeigt werden.
firstDayOfWeek .first_day_of_week(..) number 0 Der Tag, an dem die Wochen beginnen, 0 Sonntag bis 6 Samstag.
disabledMatcher .disabled_matcher(..) DateMatcher keiner Die Daten, die nicht gewählt werden können.
yearRange .year_range(..) [number, number] je 50 Jahre in beide Richtungen Die Jahre, durch die die Jahresansicht blättert, [start, end).
previousAccessibilityLabel .previous_accessibility_label(..) string keiner Was Screenreader für Zurück vorlesen.
nextAccessibilityLabel .next_accessibility_label(..) string keiner Was Screenreader für Weiter vorlesen.
headerClass, headerStyle .header_class(..), .header_style(..) string, StyleProp<ViewStyle> keiner Der Header: Zurück, die Umschalter (oder die Titel der Monate), Weiter.
bodyClass, bodyStyle .body_class(..), .body_style(..) string, StyleProp<ViewStyle> keiner Der Body: die Monate nebeneinander oder die Zeilen der Monats- bzw. Jahresansicht.
monthClass, monthStyle .month_class(..), .month_style(..) string, StyleProp<ViewStyle> keiner Jeder Monat der Tagesansicht.
rowClass, rowStyle .row_class(..), .row_style(..) string, StyleProp<ViewStyle> keiner Jede Zeile: die Wochentage, eine Woche, eine Zeile mit Monaten oder Jahren.
itemClass, itemStyle .item_class(..), .item_style(..) string, StyleProp<TextStyle> keiner Jedes Element: selected:, disabled:, focus:; bei einem Tag data-range-start:, data-range-end:, data-range-middle:, data-today:, data-outside: (Elemente).
renderItem .render_item(..) (item: CalendarEntry) => JSX.Element keiner Der Inhalt eines Elements, von der App aus dem gezeichnet, was das Element ist; andernfalls seine Nummer.
class, style Style-Methoden string, StyleProp<ViewStyle> keiner Der Kalender: der Header über dem Body.

#Events

Event Rust Beschreibung
onChange .on_change(|value| ..) Der Nutzer hat einen vollständigen Wert gewählt: { value }, einen Tag oder einen Bereich. Setze value, um ihm zu folgen. Ein von der App übergebenes value wird nie gemeldet.

#Parts

Part Wo Gestaltet mit
Der Kalender die Wurzel class, style
Der Header der Kalender, zuerst headerClass, headerStyle
Der Body der Kalender, nach dem Header bodyClass, bodyStyle
Ein Monat der Body (Tagesansicht) monthClass, monthStyle
Eine Zeile ein Monat oder der Body (Monats- und Jahresansicht) rowClass, rowStyle
Ein Element der Header und die Zeilen itemClass, itemStyle, renderItem (group-selected:, group-data-range-middle:, … darin, unter group in itemClass)

#Tastatur

Die Tage sind ein Tab-Stopp: Er landet auf dem ausgewählten Tag, sonst auf heute, sonst auf dem ersten angezeigten Tag. Danach gilt:

Taste Aktion
Links / Rechts Der Tag davor oder danach.
Hoch / Runter Derselbe Tag eine Woche davor oder danach.
Pos1 / Ende Der erste oder letzte Tag der Woche.
Bild auf / Bild ab Derselbe Tag einen Monat davor oder danach.
Umschalt+Bild auf / Umschalt+Bild ab Derselbe Tag ein Jahr davor oder danach.
Return, Leertaste Wählt den Tag aus.

In einem Rechts-nach-links-Layout laufen die Wochen von rechts, sodass Links der Tag danach und Rechts der Tag davor ist; Hoch und Runter behalten die Wochen bei. Wer über die angezeigten Monate hinaus navigiert, blättert sie weiter. Zurück, die Umschalter, Weiter, die Monate und die Jahre nehmen Tipps an. Die Tage von gpui-base nehmen nur Tipps an; die Tasten sind die von NSH, nach dem WAI-ARIA-Date-Picker-Dialog.

#Barrierefreiheit

  • Jeder Tag ist ein Button, benannt nach seinem ISO-Datum, ausgewählt, solange er das Datum oder ein Tag des Bereichs ist (die Enden eingeschlossen, wie beim Range Calendar von React Aria), und abgeblendet, solange er deaktiviert ist.
  • Zurück und Weiter sind Buttons, benannt nach previousAccessibilityLabel und nextAccessibilityLabel; die Umschalter, Monate und Jahre sind Buttons, benannt nach dem, was sie anzeigen.

Siehe Barrierefreiheit für Labels, Rollen und Ansagen im Allgemeinen.