Skip to main content

orbital_date_pickers/building_blocks/
time_range_field.rs

1//! [`TimeRangeField`] — segmented start/end time 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, TimeField, TimeFieldAppearance, TimeFieldBind};
9
10use super::field_types::{TimeRangeFieldAppearance, TimeRangeFieldBind};
11
12/// Segmented start/end time input bound to [`DateTimeRange`].
13///
14/// TimeRangeField renders two [`TimeField`](crate::TimeField) segment groups separated by an
15/// en-dash. See the crate README for range control selection.
16///
17/// # When to use
18///
19/// - Same-day shift or hours windows entered by keyboard
20/// - Compact toolbar filters for time-of-day spans
21///
22/// # Usage
23///
24/// 1. Bind `Option<DateTimeRange>` through [`TimeRangeFieldBind`].
25/// 2. Set [`TimeRangeFieldAppearance`] for 12-hour or 24-hour segments.
26/// 3. Share [`DatetimeLocale`] with sibling pickers on the page.
27///
28/// # Best Practices
29///
30/// ## Do's
31///
32/// - Use a shared reference date on both endpoints when the range never crosses midnight.
33///
34/// ## Don'ts
35///
36/// - Do not use for multi-day spans — use [`DateRangeField`](crate::DateRangeField) or [`DateTimeRangeField`](crate::DateTimeRangeField).
37///
38/// # Examples
39///
40/// ## Time range segments
41/// Default 12-hour start/end 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="time-range-field-preview">
50///         <PickerPreviewKnobs />
51///         <TimeRangeField bind=value />
52///         <div data-testid="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/// ## 24-hour format
61/// Hour/minute segments in 24-hour layout.
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="TRF-02">
69///         <TimeRangeField bind=value appearance=TimeRangeFieldAppearance::time24() />
70///     </PickerPreviewExample>
71/// }
72/// ```
73#[component_doc(
74    category = "Calendar & Time",
75    preview_slug = "time-range-field",
76    preview_label = "Time Range Field",
77    preview_icon = icondata::AiFieldTimeOutlined,
78)]
79#[component]
80pub fn TimeRangeField(
81    /// Value binding for the segmented time range input.
82    #[prop(optional, into)]
83    bind: TimeRangeFieldBind,
84    /// Format, reference date, and disabled state.
85    #[prop(optional, into)]
86    appearance: TimeRangeFieldAppearance,
87    /// Optional CSS class on the layout wrapper.
88    #[prop(optional, into)]
89    class: MaybeProp<String>,
90) -> impl IntoView {
91    let TimeRangeFieldBind { value, id, name } = bind;
92    let TimeRangeFieldAppearance {
93        format,
94        reference_date,
95        timezone,
96        minute_step,
97        disabled,
98    } = appearance;
99
100    let locale = use_datetime_locale();
101    let theme_options = use_theme_options();
102    let coordinator = use_range_coordinator(value);
103    let resolved_reference = Signal::derive(move || {
104        let explicit = reference_date.get();
105        if explicit.start_of_day() == locale.reference_date {
106            locale.reference_date
107        } else {
108            explicit
109        }
110    });
111
112    let field_appearance_start = TimeFieldAppearance {
113        format,
114        reference_date: resolved_reference,
115        timezone,
116        minute_step,
117        disabled,
118    };
119    let field_appearance_end = TimeFieldAppearance {
120        format,
121        reference_date: resolved_reference,
122        timezone,
123        minute_step,
124        disabled,
125    };
126
127    let root_class = move || {
128        let mut parts = vec![range_field_root_classes(
129            "orb-time-range-field",
130            theme_options.get().density,
131        )];
132        if let Some(extra) = class.get() {
133            if !extra.is_empty() {
134                parts.push(extra);
135            }
136        }
137        parts.join(" ")
138    };
139
140    view! {
141        <style>{picker_style_sheet()}</style>
142        <div class=root_class data-orbital-picker="">
143            <TimeField
144                bind=TimeFieldBind { value: coordinator.start.into(), id, name }
145                appearance=field_appearance_start
146            />
147            <span class="orb-picker-range-field__separator">" – "</span>
148            <TimeField
149                bind=TimeFieldBind { value: coordinator.end.into(), ..Default::default() }
150                appearance=field_appearance_end
151            />
152        </div>
153    }
154}