Skip to main content

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}