元素

Calendar

日历从按周排列的月份中选出一个日期,或者在 range 时选出一段日期:表单里的截止日期、一次住宿的几晚、一份报告的天数。它显示一个月,或并排显示几个月,用”上一个”和”下一个”翻月,并可切换到一年中各月的视图或一页年份,以便跳得更远。它就是 DatePicker 弹出层里的日历,单独放在页面中。

el-date-picker.tsx
el-date-picker.tsx运行中
import { type JSX, mount, signal, Text, token, View } from "nsh";
import { type CalendarItemState, DatePicker, type DatePickerValue, type DateRangePreset, type HourCycle, type OpenChangeReason, type TimeFieldSegmentState } from "nsh/base";
import { button, desktop, Log, Panel, Segmented, size, Split, tint, ui, wide } from "./palette";

// Three date pickers in a card: a due date (weekends disabled, presets
// beside the calendar, a clear button), a stay picked as a range over two
// months in a wide window, and a meeting's date and time with the hours
// on the clock the inspector picks. The trigger is the shared button look
// with the value and a chevron that turns while the calendar is open; the
// calendar is a popup at the trigger, a sheet on a touch screen. Every
// item is drawn by `renderItem` (month names, weekday initials, today on a
// quiet fill, the selected days filled, a range's band) and every segment
// of the time by `renderSegment`; the time's label starts its row and the
// field, a well as the number inputs', ends it.

const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
const WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];

/** A day's ISO date (`"2026-10-28"`), in local time as the calendar's days are. */
const iso = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
const addDays = (d: Date, days: number) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + days);
/** An ISO date's month and day: `Oct 28`. */
const monthDay = (value: string) => `${MONTHS[Number(value.slice(5, 7)) - 1].slice(0, 3)} ${Number(value.slice(8, 10))}`;

/** A time `"HH:MM"` on the picked clock: `14:30`, or `2:30 PM`. */
function clock(time: string, cycle: HourCycle) {
  if (cycle === "h23") return time;
  const hour = Number(time.slice(0, 2));
  return `${hour % 12 === 0 ? 12 : hour % 12}:${time.slice(3, 5)} ${hour < 12 ? "AM" : "PM"}`;
}

/** The text of a control: 13 with a pointer, the touch label size otherwise. */
const controlText = desktop ? 13 : size.label;
/** A day's cell: seven across the popup with a pointer, across a phone's sheet otherwise. */
const cell = desktop ? 32 : 42;
/** The gap between the months of a calendar of two. */
const monthGap = 16;

/** A popup at the trigger with a pointer, a sheet at the bottom on a touch screen. */
const surface = () =>
  desktop
    ? { ...ui().card, borderRadius: 12, padding: 8 }
    : { ...ui().card, borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingHorizontal: 10, paddingTop: 14, paddingBottom: 30 };

const backdrop = { backgroundColor: "rgba(0,0,0,0.45)" };

/** The popup's layout: presets in a column beside the calendar with a pointer, a row of chips above it on a phone. */
const content = desktop ? { flexDirection: "row" as const, gap: 10 } : { gap: 12 };
const presets = () =>
  desktop
    ? { gap: 2, minWidth: 104, paddingRight: 10, borderRightWidth: 1, borderColor: ui().border }
    : { flexDirection: "row" as const, flexWrap: "wrap" as const, gap: 6 };
const preset = () =>
  desktop
    ? { height: 28, paddingHorizontal: 8, borderRadius: 6, justifyContent: "center" as const, fontSize: 13, color: ui().text }
    : { height: 34, paddingHorizontal: 12, borderRadius: 17, borderWidth: 1, borderColor: ui().border, justifyContent: "center" as const, fontSize: 14, color: ui().text };
const calendar = desktop ? { padding: 2 } : { alignSelf: "center" as const };
/** The time's row under a hairline, its label at the start and the field at the end, on the calendar's edges. */
const time = () => ({
  gap: 10,
  marginTop: 8,
  paddingTop: 8,
  borderTopWidth: 1,
  borderColor: ui().border,
  ...(desktop ? { paddingHorizontal: 2 } : { alignSelf: "center" as const, width: 7 * cell }),
});
/** The time's field: a recessed well as the number inputs', with their hover and the keyboard's ring while it edits. */
const timeField = [
  desktop ? "h-8 rounded-lg px-2" : "h-11 rounded-xl px-3",
  "items-center border border-input bg-field",
  "enabled:hover:border-faint focus-within:outline-2 focus-within:outline-info",
].join(" ");

/** The trigger: the shared button's look, the value at the start and a chevron in a lane as wide as it. */
function Trigger(props: { title?: string; placeholder: string; open: boolean; clearable?: boolean }) {
  const look = button("secondary", {
    style: () => ({
      flexDirection: "row",
      justifyContent: "space-between",
      gap: 8,
      ...(desktop ? { width: 260 } : {}),
      ...(props.open ? { borderColor: ui().faint } : {}),
    }),
  });
  return (
    <View {...look}>
      <Text style={{ numberOfLines: 1, flex: 1, fontSize: controlText, color: props.title ? ui().text : ui().faint }}>
        {props.title ?? props.placeholder}
      </Text>
      {/* With a clear button over the end (`cleanable`), an empty lane as wide as the button in place of the chevron. */}
      <View
        class="transition-transform duration-150 motion-reduce:transition-none"
        style={{
          width: props.clearable ? 20 : 10,
          height: 20,
          alignItems: "center",
          justifyContent: "center",
          opacity: props.clearable ? 0 : 1,
          transform: [{ rotate: props.open ? "270deg" : "90deg" }],
        }}
      >
        <Text style={{ fontSize: 18, color: ui().muted }}>›</Text>
      </View>
    </View>
  );
}

/**
 * An item of the calendar: Previous and Next as arrows, the month and year toggles (or each month's
 * title) by name, the weekdays' initials, and the days: today on the theme's quiet `accent` fill
 * (gpui-component's), the selected ones filled,
 * the days of a complete range on a band from end to end (`range` says where it starts and ends), the
 * other months' days and the disabled ones faint.
 */
function Item(props: { item: CalendarItemState; range?: DatePickerValue }) {
  const kind = props.item.kind;
  if (kind === "previous" || kind === "next") {
    return (
      <View style={{ width: cell, height: cell, alignItems: "center", justifyContent: "center", opacity: props.item.disabled ? 0.3 : 1 }}>
        <Text style={{ fontSize: 20, color: ui().muted }}>{kind === "previous" ? "‹" : "›"}</Text>
      </View>
    );
  }
  if (kind === "monthToggle" || kind === "yearToggle") {
    // A month's title in a calendar of several takes no press: the month and its year, over the month.
    const title = kind === "monthToggle" && props.item.disabled;
    return (
      <Text
        style={{
          width: title ? 6 * cell + monthGap / 2 : kind === "monthToggle" ? 3 * cell : 2 * cell,
          paddingHorizontal: 3,
          textAlign: title ? "center" : kind === "monthToggle" ? "right" : "left",
          fontSize: size.label,
          fontWeight: "600",
          color: props.item.active ? ui().accent : ui().text,
        }}
      >
        {title ? `${MONTHS[props.item.value - 1]} ${props.item.year}` : kind === "monthToggle" ? MONTHS[props.item.value - 1] : String(props.item.value)}
      </Text>
    );
  }
  if (kind === "weekday") {
    return <Text style={{ width: cell, paddingVertical: 4, textAlign: "center", fontSize: 11, color: ui().faint }}>{WEEKDAYS[props.item.value]}</Text>;
  }
  if (kind === "month" || kind === "year") {
    // The month view's months (rows of 3) and the year view's years (rows of 5), as wide as a month.
    return (
      <View
        style={{
          width: (7 * cell) / (kind === "month" ? 3 : 5),
          height: cell + 6,
          alignItems: "center",
          justifyContent: "center",
          borderRadius: 8,
          backgroundColor: props.item.active ? ui().accentFill.backgroundColor : "transparent",
        }}
      >
        <Text style={{ fontSize: controlText, color: props.item.active ? ui().onAccent : ui().text }}>
          {kind === "month" ? MONTHS[props.item.value - 1].slice(0, 3) : String(props.item.value)}
        </Text>
      </View>
    );
  }
  // A day. The band reaches from the start's middle to the end's.
  const band = () => {
    const r = props.range;
    if (!props.item.inRange || !Array.isArray(r) || r[0] === r[1]) return null;
    return { left: props.item.date === r[0] ? cell / 2 : 0, right: props.item.date === r[1] ? cell / 2 : 0 };
  };
  return (
    <View style={{ width: cell, height: cell, alignItems: "center", justifyContent: "center" }}>
      <View style={{ position: "absolute", top: 2, bottom: 2, left: band()?.left ?? 0, right: band()?.right ?? 0, backgroundColor: band() ? tint(ui().accent, 0.14) : "transparent" }} />
      <View
        style={{
          width: cell - 4,
          height: cell - 4,
          borderRadius: (cell - 4) / 2,
          alignItems: "center",
          justifyContent: "center",
          borderWidth: 1.5,
          // The keyboard's day ringed.
          borderColor: props.item.focused ? ui().info : "transparent",
          backgroundColor: props.item.active ? ui().accentFill.backgroundColor : props.item.today ? token("accent") : "transparent",
        }}
      >
        <Text
          style={{
            fontSize: controlText,
            fontVariant: ["tabular-nums"],
            fontWeight: props.item.active || props.item.today ? "600" : "400",
            color: props.item.active ? ui().onAccent : props.item.muted ? ui().faint : ui().text,
            opacity: props.item.disabled ? 0.5 : 1,
          }}
        >
          {String(props.item.value)}
        </Text>
      </View>
    </View>
  );
}

/** A segment of the time: its digits, or AM / PM, filled while it has the keys. */
function Segment(props: { segment: TimeFieldSegmentState }) {
  return (
    <Text
      style={{
        paddingHorizontal: 3,
        borderRadius: 4,
        fontSize: controlText,
        fontVariant: ["tabular-nums"],
        color: props.segment.selected ? ui().onAccent : ui().text,
        backgroundColor: props.segment.selected ? ui().accentFill.backgroundColor : "transparent",
      }}
    >
      {props.segment.label}
    </Text>
  );
}

/** A label over its control, a quieter line under the label. */
function Field(props: { label: string; detail: string; children: JSX.Element }) {
  return (
    <View style={{ gap: 8 }}>
      <View style={{ gap: 2 }}>
        <Text style={{ fontSize: size.label, fontWeight: "500", color: ui().text }}>{props.label}</Text>
        <Text style={{ fontSize: size.detail, color: ui().muted }}>{props.detail}</Text>
      </View>
      {props.children}
    </View>
  );
}

function App() {
  const due = signal<string | null>(null);
  const stay = signal<DatePickerValue>(null);
  const meeting = signal<string | null>(null);
  const cycle = signal<HourCycle>("h12");
  const dueOpen = signal(false);
  const stayOpen = signal(false);
  const meetingOpen = signal(false);
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 8));
  const opened = (open: (next: boolean) => void) => ({ open: next }: { open: boolean; reason: OpenChangeReason }) => open(next);
  // The presets from today.
  const today = new Date();
  const monday = addDays(today, ((8 - today.getDay()) % 7) || 7);
  const dues: DateRangePreset[] = [
    { label: "Today", value: iso(today) },
    { label: "Tomorrow", value: iso(addDays(today, 1)) },
    { label: "Next Monday", value: iso(monday) },
  ];
  const stays: DateRangePreset[] = [
    { label: "This week", value: [iso(today), iso(addDays(today, (7 - today.getDay()) % 7))] },
    { label: "Next 7 nights", value: [iso(today), iso(addDays(today, 7))] },
  ];
  const stayTitle = () => {
    const r = stay();
    return Array.isArray(r) && r[0] && r[1] ? `${monthDay(r[0])} – ${monthDay(r[1])}, ${r[1].slice(0, 4)}` : undefined;
  };
  const meetingTitle = () => {
    const v = meeting();
    return v ? `${monthDay(v)}, ${v.slice(0, 4)} · ${clock(v.slice(11, 16), cycle())}` : undefined;
  };
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>date-picker</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Arrows move between days, Return picks; the trigger takes Delete to clear" : "Tap a day; the meeting's time takes digits"}
      </Text>

      <Split
        gap={14}
        inspector={
          <>
            <Panel title="value">
              <Text style={{ fontSize: 13, color: ui().text }}>{`due ${JSON.stringify(due())}`}</Text>
              <Text style={{ fontSize: 13, color: ui().text }}>{`stay ${JSON.stringify(stay())}`}</Text>
              <Text style={{ fontSize: 13, color: ui().text }}>{`meeting ${JSON.stringify(meeting())}`}</Text>
            </Panel>
            <Segmented label="hourCycle" options={["h12", "h23"] as const} value={cycle()} onChange={(v) => cycle.set(v)} />
            <Log title="onChange" lines={events()} minHeight={120} />
          </>
        }
      >
        <View style={{ ...ui().card, borderRadius: 16, padding: 16, gap: 20 }}>
          <Field label="Due" detail="Weekdays only, presets beside the calendar, a clear button">
            <DatePicker
              value={due()}
              onChange={({ value }) => {
                due.set(typeof value === "string" ? value : null);
                log(`Due → ${JSON.stringify(value)}`);
              }}
              onOpenChange={opened((next) => dueOpen.set(next))}
              accessibilityLabel="Due"
              disabledMatcher={[0, 6]}
              presets={dues}
              offset={4}
              margin={8}
              popupStyle={surface()}
              backdropStyle={backdrop}
              contentStyle={content}
              presetsStyle={presets()}
              presetStyle={preset()}
              presetClass="active:opacity-70"
              calendarStyle={calendar}
              headerStyle={{ marginBottom: 4 }}
              itemClass="active:opacity-70"
              renderItem={(item) => <Item item={item} />}
              style={desktop ? { width: 260 } : undefined}
              cleanable
              clearButtonAccessibilityLabel="Clear the due date"
              clearButtonClass="absolute items-center justify-center rounded-full enabled:hover:opacity-70"
              clearButtonStyle={{ top: (size.control - 20) / 2, right: 10, width: 20, height: 20 }}
              clearButton={<Text style={{ fontSize: 12, color: ui().muted }}>✕</Text>}
              trigger={<Trigger title={due() ? `${monthDay(due()!)}, ${due()!.slice(0, 4)}` : undefined} placeholder="No due date" open={dueOpen()} clearable={due() !== null} />}
            />
          </Field>
          <Field label="Stay" detail={wide() ? "A range over two months: the first press starts it" : "A range: the first press starts it, the second ends it"}>
            <DatePicker
              range
              value={stay()}
              onChange={({ value }) => {
                stay.set(value);
                log(`Stay → ${JSON.stringify(value)}`);
              }}
              onOpenChange={opened((next) => stayOpen.set(next))}
              accessibilityLabel="Stay"
              numberOfMonths={wide() ? 2 : 1}
              firstDayOfWeek={1}
              presets={stays}
              offset={4}
              margin={8}
              popupStyle={surface()}
              backdropStyle={backdrop}
              contentStyle={content}
              presetsStyle={presets()}
              presetStyle={preset()}
              presetClass="active:opacity-70"
              calendarStyle={calendar}
              headerStyle={{ marginBottom: 4 }}
              bodyStyle={{ gap: monthGap }}
              itemClass="active:opacity-70"
              renderItem={(item) => <Item item={item} range={stay()} />}
              trigger={<Trigger title={stayTitle()} placeholder="Check-in – check-out" open={stayOpen()} />}
            />
          </Field>
          <Field label="Meeting" detail="A day, then its time; the day again closes the calendar">
            <DatePicker
              value={meeting()}
              onChange={({ value }) => {
                meeting.set(typeof value === "string" ? value : null);
                log(`Meeting → ${JSON.stringify(value)}`);
              }}
              onOpenChange={opened((next) => meetingOpen.set(next))}
              accessibilityLabel="Meeting"
              timePrecision="minute"
              hourCycle={cycle()}
              defaultTime="09:00"
              offset={4}
              margin={8}
              popupStyle={surface()}
              backdropStyle={backdrop}
              calendarStyle={calendar}
              headerStyle={{ marginBottom: 4 }}
              itemClass="active:opacity-70"
              renderItem={(item) => <Item item={item} />}
              timeStyle={time()}
              timeLabel={<Text style={{ fontSize: controlText, color: ui().muted }}>Time</Text>}
              timeAccessibilityLabel="Meeting time"
              timeFieldClass={timeField}
              segmentClass="rounded active:opacity-70"
              separatorStyle={{ fontSize: controlText, color: ui().faint }}
              renderSegment={(segment) => <Segment segment={segment} />}
              trigger={<Trigger title={meetingTitle()} placeholder="Not scheduled" open={meetingOpen()} />}
            />
          </Field>
        </View>
      </Split>
    </View>
  );
}

mount(App);

日历负责行为:值(应用的,或它自己的)、显示的月份、视图、不能选的日期、键盘,以及屏幕阅读器听到的内容。外观由应用决定:应用为头部、主体、每个月、每一行和每个条目设置样式,并绘制条目的内容(月份和星期的名称、今天的标记、范围的色带)。

use nsh_base::{CalendarItemKind, CalendarItemState, Date, Matcher, calendar, theme};
use nsh_core::prelude::*;

const MONTHS: [&str; 12] = [
    "January", "February", "March", "April", "May", "June",
    "July", "August", "September", "October", "November", "December",
];
const WEEKDAYS: [&str; 7] = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];

fn due_date() -> impl IntoElement {
    let due = signal(Date::default());
    calendar()
        .value(move || due.get())
        .on_change(move |next: &Date| due.set(*next))
        .disabled_matcher(Matcher::DayOfWeek(vec![0, 6]))
        .previous_accessibility_label("Previous month")
        .next_accessibility_label("Next month")
        .render_item(|item: &CalendarItemState| {
            let label = match item.kind {
                CalendarItemKind::Previous => "‹".to_string(),
                CalendarItemKind::Next => "›".to_string(),
                CalendarItemKind::Weekday => WEEKDAYS[item.value as usize].to_string(),
                CalendarItemKind::MonthToggle | CalendarItemKind::Month => {
                    MONTHS[item.value as usize - 1].to_string()
                }
                _ => item.value.to_string(),
            };
            view()
                .w_8()
                .h_8()
                .items_center()
                .justify_center()
                .rounded_md()
                .when(item.active, |v| v.bg(move || theme().primary))
                .child(text(label).text_sm())
        })
}

Calendar 来自 nsh/base,在 Rust 中是 nsh-base crate。在 Rust 中,calendar() 创建一个日历,每个属性有一个设置方法(.value(..)、.range(..)、.number_of_months(..)),每个事件有一个监听方法(.on_change(..))。它的值是 Date(Date::Single、Date::Range,由一个 chrono::NaiveDate 或一对日期构成),禁用的日期是 Matcher。

#值

值是一个 ISO 日期 "2026-10-28",与 HTML <input type="date"> 的相同;在 range 时是 [start, end],null 表示没有日期。有 value 时,值由应用持有:日历把它显示为已选,并用 onChange({ value }) 报告一次选择;应用把 value 设为报告的值以跟随它。省略 value 时,日历自己保存值,初始为 defaultValue。

onChange 只报告完整的值,与 gpui-base 的 Selected 相同:按下的一天,或范围的结束日。

  • 不设 range 时,按下某一天就选中它。
  • 在 range 时,第一次按压开始范围,第二次结束范围,两者之间的日期都在范围内(每一天的 inRange,包括两端)。第二次按在更早的一天上会从那一天重新开始范围,范围完整之后再按也一样。半个范围会一直显示,直到完整时才被报告。

日历打开时显示值所在的月份(范围的开始日),没有值则显示今天所在的月份。

#月份与视图

numberOfMonths 从显示的月份起并排显示这么多个月;“上一个”和”下一个”每次翻一个月。firstDayOfWeek 是每周的第一天,0 为星期日(默认)到 6 为星期六。

头部包含”上一个”、两个切换按钮和”下一个”:

  • 日视图显示各月的日期,每个月是一列,包含星期行和各周;前后月份的日期补满各周,并为 muted。
  • 月份切换按钮(monthToggle)打开月份视图:十二个月,每行三个。选中一个会在日视图中显示它。
  • 年份切换按钮(yearToggle)打开年份视图:一页 20 年,每行五个,“上一个”和”下一个”翻页。选中一个会在日视图中显示那一年。yearRange 是翻页经过的年份,[start, end)(默认:今年之前 50 年到之后 50 年)。

显示多个月时,头部用每个月的标题代替切换按钮(一个不接受按压的 monthToggle 条目),与 gpui-base 的相同。

#禁用的日期

disabledMatcher 是用户不能选的日期,即 gpui-base 的 Matcher,有四种形式:

形式禁用
[0, 6]这些星期几,0 为星期日到 6 为星期六。
{ before, after }before 之前和 after 之后的日期(任一可省略)。
{ from, to }从 from 到 to 的日期,包括两端(省略的一端不限)。
(date) => boolean函数返回 true 的日期,传入每个显示日期的 ISO 日期。

函数对每个显示的日期调用一次,在下一个 JS 轮次中调用,所以它的结果应只取决于日期。被禁用的日期为 disabled 和 muted,不接受按压,Return 和 Space 也不会选中它。

#条目

日历中每个可按压的目标和标签都是一个条目:“上一个”、切换按钮、“下一个”、星期、日期、月份和年份。每个条目绘制在一个由 itemClass 和 itemStyle 设置样式的部件中,其变体跟随状态:选中时应用 selected:(日期或范围的一端、显示的月份或年份、已打开视图的切换按钮),不接受按压时应用 disabled:,键盘在它上面时应用 focus:。

renderItem 根据条目的 CalendarItemState 绘制其内容:

字段含义
kind"previous"、"monthToggle"、"yearToggle"、"next"、"weekday"、"day"、"month" 或 "year"。
value它的数字:一个月中的第几天、月份(1 到 12)、年份、星期几(0 为星期日到 6);“上一个”和”下一个”为 0。
date某一天的 ISO 日期。
year、month它所属的年份和月份,否则为显示的年月。
active它被选中。
inRange完整范围内的一天,包括两端。
muted星期、前后月份的日期、被禁用的日期。
disabled它不接受按压。
today这一天是今天。
focused键盘在这一天上。

不设 renderItem 时,条目显示它的数字,“上一个”和”下一个”显示 ‹ 和 ›,与 gpui-base 的相同;星期也显示它的数字 0 到 6,因为月份和星期的名称是应用用自己的语言说的话。

#属性

属性Rust类型默认值说明
value.value(..)CalendarValue无值,由应用持有。
defaultValue.default_value(..)CalendarValuenull没有 value 时的初始值。
range.range(..)booleanfalse选择范围:第一次按压开始,第二次结束。
numberOfMonths.number_of_months(..)number1并排显示几个月。
firstDayOfWeek.first_day_of_week(..)number0每周的第一天,0 为星期日到 6 为星期六。
disabledMatcher.disabled_matcher(..)DateMatcher无不能选的日期。
yearRange.year_range(..)[number, number]前后各 50 年年份视图翻页经过的年份,[start, end)。
previousAccessibilityLabel.previous_accessibility_label(..)string无屏幕阅读器对”上一个”的称呼。
nextAccessibilityLabel.next_accessibility_label(..)string无屏幕阅读器对”下一个”的称呼。
headerClass、headerStyle.header_class(..)、.header_style(..)string、StyleProp<ViewStyle>无头部:“上一个”、切换按钮(或各月标题)、“下一个”。
bodyClass、bodyStyle.body_class(..)、.body_style(..)string、StyleProp<ViewStyle>无主体:并排的各月,或月份、年份视图的行。
monthClass、monthStyle.month_class(..)、.month_style(..)string、StyleProp<ViewStyle>无日视图中的每个月。
rowClass、rowStyle.row_class(..)、.row_style(..)string、StyleProp<ViewStyle>无每一行:星期、一周、一行月份或年份。
itemClass、itemStyle.item_class(..)、.item_style(..)string、StyleProp<TextStyle>无每个条目:selected:、disabled:、focus:。
renderItem.render_item(..)(item: CalendarItemState) => JSX.Element无由应用绘制的条目内容;否则显示其数字。
class、style样式方法string、StyleProp<ViewStyle>无日历:头部在主体之上。

#事件

事件Rust说明
onChange.on_change(|value| ..)用户选出了一个完整的值:{ value },一天或一个范围。设置 value 以跟随它。应用传入的 value 从不被报告。

#部件

部件位置样式
日历根class、style
头部日历中的第一个headerClass、headerStyle
主体日历中,头部之后bodyClass、bodyStyle
月主体中(日视图)monthClass、monthStyle
行月中,或主体中(月份和年份视图)rowClass、rowStyle
条目头部和各行中itemClass、itemStyle、renderItem

#键盘

所有日期是 Tab 的一站:落在选中的日期上,没有则落在今天,再没有则落在第一个显示的日期。然后:

按键操作
Left / Right前一天或后一天。
Up / Down一周前或一周后的同一天。
Home / End本周的第一天或最后一天。
Page Up / Page Down一个月前或一个月后的同一天。
Shift+Page Up / Shift+Page Down一年前或一年后的同一天。
Return、Space选中这一天。

移出显示的月份时会翻月。“上一个”、切换按钮、“下一个”、月份和年份接受按压。gpui-base 的日期只接受按压;这些按键是 NSH 的,参照 WAI-ARIA 的日期选择对话框。

#无障碍

  • 每一天是一个按钮,以其 ISO 日期命名;它是选中的日期或范围的一端时为已选状态,被禁用时显示为不可用。
  • “上一个”和”下一个”是按钮,名称来自 previousAccessibilityLabel 和 nextAccessibilityLabel;切换按钮、月份和年份是按钮,以其显示的内容命名。

关于标签、角色和播报的一般说明,见无障碍。