Skip to main content

orbital_date_pickers/building_blocks/date_range_field/
mod.rs

1//! [`DateRangeField`] — segmented start/end date input bound to [`DateTimeRange`].
2
3mod styles;
4
5use leptos::prelude::*;
6use orbital_macros::component_doc;
7use orbital_theme::use_theme_options;
8
9use crate::shared::{picker_style_sheet, range_field_root_classes, use_range_coordinator};
10use crate::{use_datetime_locale, DateField, DateFieldAppearance, DateFieldBind};
11
12use super::field_types::{DateRangeFieldAppearance, DateRangeFieldBind};
13use styles::date_range_field_styles;
14
15/// Segmented start/end date input bound to [`DateTimeRange`].
16///
17/// DateRangeField renders two [`DateField`](crate::DateField) segment groups separated by an
18/// en-dash. Values commit when both endpoints parse successfully. See
19/// See the crate README for field vs picker choice.
20///
21/// # When to use
22///
23/// - Dense forms where users type dates instead of opening a calendar panel
24/// - Tables or filters that need compact start/end segments
25///
26/// # Usage
27///
28/// 1. Bind `Option<DateTimeRange>` through [`DateRangeFieldBind`].
29/// 2. Inherit format and timezone from [`DatetimeLocale`] when wrapped.
30/// 3. Pair with [`DateRangePicker`](crate::DateRangePicker) when users also need a calendar popover.
31///
32/// # Best Practices
33///
34/// ## Do's
35///
36/// - Show validation when end precedes start — the field commits only when both sides parse.
37///
38/// ## Don'ts
39///
40/// - Do not use for time-only spans — use [`TimeRangeField`](crate::TimeRangeField) instead.
41///
42/// # Examples
43///
44/// ## Range segments
45/// Default US-format start/end segments with bind readout for E2E.
46/// <!-- preview -->
47/// ```rust
48/// use crate::DateTimeRange;
49/// use orbital_base_components::ToUnixSeconds;
50/// use crate::preview::{PickerPreviewExample, PickerPreviewKnobs};
51/// let value = RwSignal::new(None::<DateTimeRange>);
52/// view! {
53///     <PickerPreviewExample data_testid="date-range-field-preview">
54///         <PickerPreviewKnobs />
55///         <DateRangeField bind=value />
56///         <div data-testid="date-range-field-preview-VALUE">{move || match value.get() {
57///             Some(range) => [range.start.to_unix_seconds().to_string(), range.end.to_unix_seconds().to_string()].join(","),
58///             None => "none".to_string(),
59///         }}</div>
60///     </PickerPreviewExample>
61/// }
62/// ```
63///
64/// ## Bind readout
65/// Completed start and end segments update the bound range.
66/// <!-- preview -->
67/// ```rust
68/// use crate::DateTimeRange;
69/// use orbital_base_components::ToUnixSeconds;
70/// use crate::preview::PickerPreviewExample;
71/// let value = RwSignal::new(None::<DateTimeRange>);
72/// view! {
73///     <PickerPreviewExample data_testid="DRF-02">
74///         <DateRangeField bind=value />
75///         <div data-testid="DRF-02-VALUE">{move || match value.get() {
76///             Some(range) => [range.start.to_unix_seconds().to_string(), range.end.to_unix_seconds().to_string()].join(","),
77///             None => "none".to_string(),
78///         }}</div>
79///     </PickerPreviewExample>
80/// }
81/// ```
82#[component_doc(
83    category = "Calendar & Time",
84    preview_slug = "date-range-field",
85    preview_label = "Date Range Field",
86    preview_icon = icondata::AiFieldBinaryOutlined,
87)]
88#[component]
89pub fn DateRangeField(
90    /// Value binding for the segmented range input.
91    #[prop(optional, into)]
92    bind: DateRangeFieldBind,
93    /// Format, timezone, and disabled state.
94    #[prop(optional, into)]
95    appearance: DateRangeFieldAppearance,
96    /// Optional CSS class on the layout wrapper.
97    #[prop(optional, into)]
98    class: MaybeProp<String>,
99) -> impl IntoView {
100    let DateRangeFieldBind { value, id, name } = bind;
101    let DateRangeFieldAppearance {
102        format,
103        timezone,
104        disabled,
105    } = appearance;
106
107    let locale = use_datetime_locale();
108    let theme_options = use_theme_options();
109    let coordinator = use_range_coordinator(value);
110
111    let field_appearance_start = DateFieldAppearance {
112        format,
113        timezone,
114        disabled,
115    };
116    let field_appearance_end = DateFieldAppearance {
117        format,
118        timezone,
119        disabled,
120    };
121
122    let root_class = move || {
123        let mut parts = vec![range_field_root_classes(
124            "orb-date-range-field",
125            theme_options.get().density,
126        )];
127        if let Some(extra) = class.get() {
128            if !extra.is_empty() {
129                parts.push(extra);
130            }
131        }
132        let _ = locale.default_format;
133        parts.join(" ")
134    };
135
136    view! {
137        <style>{date_range_field_styles()}</style>
138        <style>{picker_style_sheet()}</style>
139        <div class=root_class data-orbital-picker="">
140            <DateField
141                bind=DateFieldBind { value: coordinator.start.into(), id, name }
142                appearance=field_appearance_start
143            />
144            <span class="orb-picker-range-field__separator">" – "</span>
145            <DateField
146                bind=DateFieldBind { value: coordinator.end.into(), ..Default::default() }
147                appearance=field_appearance_end
148            />
149        </div>
150    }
151}