orbital_date_pickers/building_blocks/date_time_range_field.rs
1//! [`DateTimeRangeField`] — segmented start/end datetime input bound to [`DateTimeRange`].
2
3use leptos::prelude::*;
4use orbital_macros::component_doc;
5use orbital_theme::use_theme_options;
6
7use crate::shared::{picker_style_sheet, range_field_root_classes, use_range_coordinator};
8use crate::{use_datetime_locale, DateTimeField, DateTimeFieldAppearance, DateTimeFieldBind};
9
10use super::field_types::{DateTimeRangeFieldAppearance, DateTimeRangeFieldBind};
11
12/// Segmented start/end datetime input bound to [`DateTimeRange`].
13///
14/// DateTimeRangeField renders two [`DateTimeField`](crate::DateTimeField) segment groups
15/// separated by an en-dash. See the crate README for range control selection.
16///
17/// # When to use
18///
19/// - Forms that need typed date+time on both endpoints without popover panels
20/// - Accessibility-first flows where segmented fields beat mouse-driven pickers
21///
22/// # Usage
23///
24/// 1. Bind `Option<DateTimeRange>` through [`DateTimeRangeFieldBind`].
25/// 2. Configure [`DateTimeRangeFieldAppearance`] for date and time format independently.
26/// 3. Wrap in [`DatetimeLocale`](crate::DatetimeLocale) for default timezone.
27///
28/// # Best Practices
29///
30/// ## Do's
31///
32/// - Pair with [`DateTimeRangePicker`](crate::DateTimeRangePicker) when users want both typing and panels.
33///
34/// ## Don'ts
35///
36/// - Do not convert to unix in the bind — keep [`OrbitalDateTime`] endpoints inside [`DateTimeRange`].
37///
38/// # Examples
39///
40/// ## Datetime range field
41/// Default US date + 12-hour time segments with bind readout.
42/// <!-- preview -->
43/// ```rust
44/// use crate::DateTimeRange;
45/// use orbital_base_components::ToUnixSeconds;
46/// use crate::preview::{PickerPreviewExample, PickerPreviewKnobs};
47/// let value = RwSignal::new(None::<DateTimeRange>);
48/// view! {
49/// <PickerPreviewExample data_testid="date-time-range-field-preview">
50/// <PickerPreviewKnobs />
51/// <DateTimeRangeField bind=value />
52/// <div data-testid="date-time-range-field-preview-VALUE">{move || match value.get() {
53/// Some(range) => [range.start.to_unix_seconds().to_string(), range.end.to_unix_seconds().to_string()].join(","),
54/// None => "none".to_string(),
55/// }}</div>
56/// </PickerPreviewExample>
57/// }
58/// ```
59///
60/// ## ISO date and 24-hour time
61/// Year-month-day segments with 24-hour hour/minute endpoints.
62/// <!-- preview -->
63/// ```rust
64/// use crate::DateTimeRange;
65/// use crate::preview::PickerPreviewExample;
66/// let value = RwSignal::new(None::<DateTimeRange>);
67/// view! {
68/// <PickerPreviewExample data_testid="DTRF-02">
69/// <DateTimeRangeField bind=value appearance=DateTimeRangeFieldAppearance::iso_time24() />
70/// </PickerPreviewExample>
71/// }
72/// ```
73#[component_doc(
74 category = "Calendar & Time",
75 preview_slug = "date-time-range-field",
76 preview_label = "DateTime Range Field",
77 preview_icon = icondata::AiFieldTimeOutlined,
78)]
79#[component]
80pub fn DateTimeRangeField(
81 /// Value binding for the segmented datetime range input.
82 #[prop(optional, into)]
83 bind: DateTimeRangeFieldBind,
84 /// Date format, time format, timezone, and disabled state.
85 #[prop(optional, into)]
86 appearance: DateTimeRangeFieldAppearance,
87 /// Optional CSS class on the layout wrapper.
88 #[prop(optional, into)]
89 class: MaybeProp<String>,
90) -> impl IntoView {
91 let DateTimeRangeFieldBind { value, id, name } = bind;
92 let DateTimeRangeFieldAppearance {
93 date_format,
94 time_format,
95 timezone,
96 disabled,
97 } = appearance;
98
99 let _locale = use_datetime_locale();
100 let theme_options = use_theme_options();
101 let coordinator = use_range_coordinator(value);
102
103 let field_appearance_start = DateTimeFieldAppearance {
104 date_format,
105 time_format,
106 timezone,
107 disabled,
108 };
109 let field_appearance_end = DateTimeFieldAppearance {
110 date_format,
111 time_format,
112 timezone,
113 disabled,
114 };
115
116 let root_class = move || {
117 let mut parts = vec![range_field_root_classes(
118 "orb-datetime-range-field",
119 theme_options.get().density,
120 )];
121 if let Some(extra) = class.get() {
122 if !extra.is_empty() {
123 parts.push(extra);
124 }
125 }
126 parts.join(" ")
127 };
128
129 view! {
130 <style>{picker_style_sheet()}</style>
131 <div class=root_class data-orbital-picker="">
132 <DateTimeField
133 bind=DateTimeFieldBind { value: coordinator.start.into(), id, name }
134 appearance=field_appearance_start
135 />
136 <span class="orb-picker-range-field__separator">" – "</span>
137 <DateTimeField
138 bind=DateTimeFieldBind { value: coordinator.end.into(), ..Default::default() }
139 appearance=field_appearance_end
140 />
141 </div>
142 }
143}