orbital_date_pickers/pickers/date_time_range_picker.rs
1//! [`DateTimeRangePicker`] — datetime range field with dual datetime picker panels.
2
3use leptos::prelude::*;
4use orbital_macros::component_doc;
5use orbital_theme::use_theme_options;
6
7use crate::building_blocks::{
8 DateTimeRangeField, DateTimeRangeFieldAppearance, DateTimeRangeFieldBind,
9 DateTimeRangePickerAppearance, DateTimeRangePickerBind,
10};
11use crate::pickers::DateTimePicker;
12use crate::shared::{
13 datetime_range_picker_row_class, layout_root_classes, picker_style_sheet, use_range_coordinator,
14};
15use crate::{DateTimePickerAppearance, DateTimePickerBind};
16
17/// Datetime range field with side-by-side start/end [`DateTimePicker`](crate::DateTimePicker)
18/// panels, bound to [`DateTimeRange`].
19///
20/// See the crate README for range control selection.
21///
22/// # When to use
23///
24/// - Meeting slot booking with full date and time on both endpoints
25/// - Maintenance windows that span partial days
26///
27/// # Usage
28///
29/// 1. Wrap in [`DatetimeLocale`](crate::DatetimeLocale) for timezone and format defaults.
30/// 2. Bind `Option<DateTimeRange>` through [`DateTimeRangePickerBind`].
31/// 3. Tune [`DateTimeRangePickerAppearance`] for date masks and 12/24-hour time columns.
32///
33/// # Best Practices
34///
35/// ## Do's
36///
37/// - Validate `range.end` is after `range.start` before submit — the picker allows partial entry while editing.
38///
39/// ## Don'ts
40///
41/// - Do not store unix seconds in the bind — use [`DateTimeRange`] with [`OrbitalDateTime`] endpoints.
42///
43/// # Examples
44///
45/// ## Meeting slot range
46/// Default US date + 12-hour time range with bind readout.
47/// <!-- preview -->
48/// ```rust
49/// use crate::DateTimeRange;
50/// use orbital_base_components::ToUnixSeconds;
51/// use crate::preview::{PickerPreviewExample, PickerPreviewKnobs};
52/// let value = RwSignal::new(None::<DateTimeRange>);
53/// view! {
54/// <PickerPreviewExample data_testid="date-time-range-picker-preview">
55/// <PickerPreviewKnobs />
56/// <DateTimeRangePicker bind=value />
57/// <div data-testid="date-time-range-picker-preview-VALUE">{move || match value.get() {
58/// Some(range) => [range.start.to_unix_seconds().to_string(), range.end.to_unix_seconds().to_string()].join(","),
59/// None => "none".to_string(),
60/// }}</div>
61/// </PickerPreviewExample>
62/// }
63/// ```
64///
65/// ## ISO date and 24-hour time
66/// Combined pickers with ISO date masks and 24-hour time columns.
67/// <!-- preview -->
68/// ```rust
69/// use crate::DateTimeRange;
70/// use crate::preview::PickerPreviewExample;
71/// let value = RwSignal::new(None::<DateTimeRange>);
72/// view! {
73/// <PickerPreviewExample data_testid="DTRP-02">
74/// <DateTimeRangePicker bind=value appearance=DateTimeRangePickerAppearance::iso_time24() />
75/// </PickerPreviewExample>
76/// }
77/// ```
78#[component_doc(
79 category = "Calendar & Time",
80 preview_slug = "date-time-range-picker",
81 preview_label = "DateTime Range Picker",
82 preview_icon = icondata::AiCalendarOutlined,
83)]
84#[component]
85pub fn DateTimeRangePicker(
86 /// Value binding for the combined datetime range pickers.
87 #[prop(optional, into)]
88 bind: DateTimeRangePickerBind,
89 /// Date format, time format, timezone, and disabled state.
90 #[prop(optional, into)]
91 appearance: DateTimeRangePickerAppearance,
92 /// Optional CSS class on the layout wrapper.
93 #[prop(optional, into)]
94 class: MaybeProp<String>,
95) -> impl IntoView {
96 let DateTimeRangePickerBind { value, id, name } = bind;
97 let DateTimeRangePickerAppearance {
98 date_format,
99 time_format,
100 timezone,
101 disabled,
102 } = appearance;
103
104 let theme_options = use_theme_options();
105 let value_stored = StoredValue::new(value);
106 let coordinator = use_range_coordinator(value_stored.with_value(|v| v.clone()));
107
108 let field_bind = DateTimeRangeFieldBind {
109 value: value_stored.with_value(|v| v.clone()),
110 id,
111 name,
112 };
113 let field_appearance = DateTimeRangeFieldAppearance {
114 date_format,
115 time_format,
116 timezone,
117 disabled,
118 };
119
120 let picker_appearance_start = DateTimePickerAppearance {
121 date_format,
122 time_format,
123 timezone,
124 disabled,
125 ..Default::default()
126 };
127 let picker_appearance_end = DateTimePickerAppearance {
128 date_format,
129 time_format,
130 timezone,
131 disabled,
132 ..Default::default()
133 };
134
135 let root_class = move || {
136 let mut parts = vec![layout_root_classes(theme_options.get().density)];
137 if let Some(extra) = class.get() {
138 if !extra.is_empty() {
139 parts.push(extra);
140 }
141 }
142 parts.join(" ")
143 };
144
145 view! {
146 <style>{picker_style_sheet()}</style>
147 <div class=root_class data-orbital-picker="">
148 <DateTimeRangeField bind=field_bind appearance=field_appearance />
149 <div class=datetime_range_picker_row_class()>
150 <DateTimePicker
151 bind=DateTimePickerBind { value: coordinator.start.into(), id, name }
152 appearance=picker_appearance_start
153 />
154 <DateTimePicker
155 bind=DateTimePickerBind { value: coordinator.end.into(), ..Default::default() }
156 appearance=picker_appearance_end
157 />
158 </div>
159 </div>
160 }
161}