元素
DatePicker
日期选择器从应用的触发元素打开一个 Calendar 来设置日期:截止日期、生日,在 range 时是一次旅行的日期,在 timePrecision 时是一个日期加一天中的时间。与 Select 一样,它按每种设备的习惯打开:
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);- 在桌面平台和桌面浏览器上,是触发元素处的弹出层;打开时键盘落在日期上。
- 在手机和平板上,是屏幕底部、覆盖在背景层之上的 sheet。
选择器负责行为:值(应用的,或它自己的)、展开状态、日历、时间、预设、键盘,以及屏幕阅读器听到的内容。外观由应用决定:应用绘制触发元素(显示值、占位文字、图标),并为弹出层、日历的部件、预设和时间输入框设置样式。
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)),
)
}import { mount, signal, Text, View } from "nsh";
import { DatePicker, type DatePickerValue } from "nsh/base";
function App() {
const due = signal<DatePickerValue>(null);
return (
<DatePicker
value={due()}
onChange={({ value }) => due.set(value)}
timePrecision="minute"
cleanable
clearButton={<Text>×</Text>}
clearButtonAccessibilityLabel="Clear the due date"
presets={[{ label: "Tomorrow 9:00", value: "2026-10-29T09:00" }]}
accessibilityLabel="Due"
offset={4}
popupClass="rounded-lg border border-border bg-popover p-3 text-popover-foreground shadow-md"
backdropClass="bg-black/40"
contentClass="flex-row gap-3"
presetClass="rounded-md px-2 py-1.5 text-sm hover:bg-accent"
headerClass="flex-row items-center justify-between"
rowClass="flex-row justify-between"
itemClass="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 disabled:opacity-50"
timeClass="mt-2 gap-3"
timeLabel={<Text class="text-sm">Time</Text>}
timeAccessibilityLabel="Due time"
timeFieldClass="flex-row rounded-md border border-input px-2 py-1"
segmentClass="text-sm tabular-nums focus:bg-accent"
trigger={
<View class="h-9 w-56 justify-center rounded-md border border-input px-3 hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring">
<Text>{typeof due() === "string" ? (due() as string).replace("T", " ") : "Pick a date"}</Text>
</View>
}
/>
);
}
mount(App);DatePicker 来自 nsh/base,在 Rust 中是 nsh-base crate。在 Rust 中,date_picker() 创建一个日期选择器,每个属性有一个设置方法(.value(..)、.range(..)、.time_precision(..)),每个事件有一个监听方法(.on_change(..))。它的值是 DateTime(DateTime::Single、DateTime::Range,由 chrono 的日期或日期时间构成);onChange 给出一个 PickedDate,即值及选择器的时间精度。
#值
值采用 HTML 输入框的格式:ISO 日期 "2026-10-28",与 <input type="date"> 的相同;在 range 时为 [start, end];在 timePrecision 时为日期和时间 "2026-10-28T09:30"(精确到秒时为 "2026-10-28T09:30:15"),与 <input type="datetime-local"> 的相同;null 表示没有。有 value 时,值由应用持有:选择器显示它,并用 onChange({ value }) 报告每次变化;应用把 value 设为报告的值以跟随它。省略 value 时,选择器自己保存值,初始为 defaultValue。
- 单个日期:按下某一天会设置值并关闭弹出层。
- 在
range时,第一次按压开始范围,第二次结束范围,与日历的相同;完整的范围设置值并关闭弹出层。 - 在
timePrecision时(仅限单个日期),日历下方显示一个时间输入框。选中某一天时弹出层保持打开,并报告带时间的日期;时间的每次编辑也会报告;再次按下已选中的日期会关闭弹出层。在设置任何时间之前选中的日期使用defaultTime(默认00:00)。hourCycle是时间输入框的小时计法。
cleanable 会在有值时加一个清除按钮:按下它会清除值(onChange({ value: null }))。这个按钮在触发元素之后,不在它里面,所以按它不会打开弹出层;由应用摆放并填充内容(clearButton),和选择框的清除按钮一样。
#预设
presets 是日历旁边设置值的按钮:“今天”、“最近 7 天”、“明天 9 点”。每个是 { label, value },value 为一个日期、一个范围或日期时间,按原样保存;按下它会设置值并关闭弹出层。contentClass 排列预设列与日历和时间(一行,flex-row);presetsClass 为这一列设置样式,presetClass 为每个按钮设置样式。
#日历
Calendar 除值以外的每个属性都会传给选择器的日历:numberOfMonths、firstDayOfWeek、disabledMatcher、yearRange、previousAccessibilityLabel、nextAccessibilityLabel、各部件(从 headerClass 到 itemStyle)和 renderItem。calendarClass 和 calendarStyle 为日历本身设置样式。它的视图和按键与日历的相同。
时间行(timeClass、timeStyle)是一行,其中各项垂直居中、两端分布(flex-row items-center justify-between,与 gpui-component 的相同):开头是 timeLabel,即应用为它写的文字(“时间”),末尾是时间输入框(timeFieldClass、timeFieldStyle、timeAccessibilityLabel),其各段与 TimeField 的相同(segmentClass、separatorClass、renderSegment)。
#属性
| 属性 | Rust | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
value | .value(..) | DatePickerValue | 无 | 值,由应用持有。 |
defaultValue | .default_value(..) | DatePickerValue | null | 没有 value 时的初始值。 |
range | .range(..) | boolean | false | 选择一段日期。 |
open、defaultOpen | .open(..)、.default_open(..) | boolean | false | 展开状态,由应用或选择器自己持有。 |
disabled | .disabled(..) | boolean | false | 保持关闭,不接受按压和键盘焦点。 |
accessibilityLabel | .accessibility_label(..) | string | 无 | 屏幕阅读器对它的称呼。 |
dateFormat | .date_format(..) | string | "%Y/%m/%d" 加时间 | 屏幕阅读器听到的值的格式,采用 chrono 的格式。 |
timePrecision | .time_precision(..) | "minute" | "second" | 无 | 同时编辑一天中的时间,精确到此单位。 |
hourCycle | .hour_cycle(..) | "h23" | "h12" | "h23" | 时间输入框的小时计法。 |
defaultTime | .default_time(..) | string | "00:00" | 用户设置时间之前日期使用的时间。 |
cleanable | .cleanable(..) | boolean | false | 有值时显示清除按钮。 |
presets | .presets(..) | DateRangePreset[] | [] | 弹出层中设置值的按钮。 |
numberOfMonths、firstDayOfWeek、disabledMatcher、yearRange | 同 Calendar | 同 Calendar | 同 Calendar | 日历的月数、每周起始日、禁用的日期和年份。 |
previousAccessibilityLabel、nextAccessibilityLabel | 同 Calendar | string | 无 | 屏幕阅读器对日历的”上一个”和”下一个”的称呼。 |
placement、align、offset、margin | .placement(..) 等 | 同 Select | "bottom"、"start"、0、0 | 桌面平台:弹出层相对触发元素的位置。 |
popupClass、popupStyle | .popup_class(..)、.popup_style(..) | string、StyleProp<ViewStyle> | 无 | 弹出层(桌面平台)或 sheet(触屏设备)。 |
backdropClass、backdropStyle | .backdrop_class(..)、.backdrop_style(..) | string、StyleProp<ViewStyle> | 无 | sheet 后面的背景层。 |
contentClass、contentStyle | .content_class(..)、.content_style(..) | string、StyleProp<ViewStyle> | 无 | 弹出层的内容:预设在日历和时间旁边。 |
presetsClass、presetsStyle | .presets_class(..)、.presets_style(..) | string、StyleProp<ViewStyle> | 无 | 预设列。 |
presetClass、presetStyle | .preset_class(..)、.preset_style(..) | string、StyleProp<TextStyle> | 无 | 每个预设按钮。 |
calendarClass、calendarStyle | .calendar_class(..)、.calendar_style(..) | string、StyleProp<ViewStyle> | 无 | 日历。 |
headerClass … itemStyle | 同 Calendar | 同 Calendar | 无 | 日历的头部、主体、各月、各行和各条目。 |
renderItem | .render_item(..) | (item: CalendarItemState) => JSX.Element | 无 | 由应用绘制的日历条目。 |
timeClass、timeStyle | .time_class(..)、.time_style(..) | string、StyleProp<ViewStyle> | 无 | 时间行:开头是它的标签,末尾是输入框。 |
timeLabel | .time_label(..) | JSX.Element | 无 | 时间行在输入框前显示的内容。 |
timeFieldClass、timeFieldStyle | .time_field_class(..)、.time_field_style(..) | string、StyleProp<ViewStyle> | 无 | 时间输入框。 |
timeAccessibilityLabel | .time_accessibility_label(..) | string | 无 | 屏幕阅读器对时间输入框的称呼。 |
segmentClass、segmentStyle、separatorClass、separatorStyle、renderSegment | 同 TimeField | 同 TimeField | 无 | 时间输入框的各段和 :。 |
clearButtonClass、clearButtonStyle | .clear_button_class(..)、.clear_button_style(..) | string、StyleProp<ViewStyle> | 无 | 清除按钮(cleanable)。 |
clearButton | .clear_button(..) | JSX.Element | 无 | 清除按钮的内容。 |
clearButtonAccessibilityLabel | .clear_button_accessibility_label(..) | string | 无 | 屏幕阅读器对清除按钮的称呼。 |
trigger | .trigger(..) | JSX.Element | 无 | 按下时打开弹出层的元素;弹出层相对它放置。 |
class、style | 样式方法 | string、StyleProp<ViewStyle> | 无 | 包住触发元素的视图。 |
#事件
| 事件 | Rust | 说明 |
|---|---|---|
onChange | .on_change(|value| ..) | 值变化了(一天、范围的结束日、一个预设、时间、清除按钮):{ value }。设置 value 以跟随它。 |
onOpenChange | .on_open_change(|e| ..) | 弹出层打开或关闭了,以及原因({ open, reason })。设置 open 以跟随它。 |
onOpen、onClose | .on_open(..)、.on_close(..) | 弹出层已显示,或已消失。 |
onCancel | .on_cancel(..) | 用户没有做出选择就关闭了弹出层(Escape 或返回、在外面或背景层上按压)。 |
#部件
| 部件 | 位置 | 样式 |
|---|---|---|
| 触发元素 | 选择器的视图 | 应用自己的元素(trigger) |
| 清除按钮 | 选择器的视图,触发元素之后(cleanable,有值时) | clearButtonClass、clearButtonStyle;内容 clearButton |
| 弹出层或 sheet | 在一切之上,位于触发元素处(桌面平台)或底部(触屏设备) | popupClass、popupStyle |
| 背景层 | sheet 后面(触屏设备) | backdropClass、backdropStyle |
| 内容 | 弹出层中 | contentClass、contentStyle |
| 预设 | 内容中的第一个(presets) | presetsClass、presetsStyle;每个 presetClass、presetStyle |
| 日历 | 内容中 | calendarClass、calendarStyle,以及 Calendar 的部件 |
| 时间行 | 日历下方(timePrecision) | timeClass、timeStyle;标签 timeLabel |
| 时间输入框 | 时间行中 | timeFieldClass、timeFieldStyle,以及 TimeField 的部件 |
#键盘
| 按键 | 操作 |
|---|---|
| Return(在触发元素上) | 打开或关闭弹出层。 |
| Delete、Backspace(在触发元素上) | 弹出层关闭时清除值。 |
| Escape | 不做选择就关闭弹出层。 |
在弹出层中,日历的按键在日期间移动并选中一天(见键盘),时间输入框的按键编辑时间(见键盘)。
#无障碍
- 触发元素对屏幕阅读器来说是一个组合框(combobox):它的标签是
accessibilityLabel,值是按dateFormat写出的日期(默认2026/10/28,在timePrecision时带时间),并说明弹出层是否展开。disabled时它显示为不可用。 - 日历的日期是按钮,以其 ISO 日期命名(见无障碍);预设是按钮,以其标签命名;时间输入框的名称来自
timeAccessibilityLabel。 - 清除按钮是一个按钮,名称来自
clearButtonAccessibilityLabel。
关于标签、角色和播报的一般说明,见无障碍。