Skip to main content

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}