Skip to main content

orbital_date_pickers/pickers/
time_range_picker.rs

1//! [`TimeRangePicker`] — time range field with dual time picker panels.
2
3use leptos::prelude::*;
4use orbital_core_components::{TimePicker, TimePickerAppearance, TimePickerBind};
5use orbital_macros::component_doc;
6use orbital_theme::use_theme_options;
7
8use crate::building_blocks::{
9    TimeRangeField, TimeRangeFieldAppearance, TimeRangeFieldBind, TimeRangePickerAppearance,
10    TimeRangePickerBind,
11};
12use crate::shared::{
13    datetime_range_picker_row_class, layout_root_classes, picker_style_sheet, use_range_coordinator,
14};
15
16/// Time range field with side-by-side start/end [`TimePicker`](orbital_core_components::TimePicker)
17/// panels, bound to [`DateTimeRange`].
18///
19/// See the crate README for range control selection.
20///
21/// # When to use
22///
23/// - Business hours, shift windows, or same-day time spans
24/// - Forms where users pick start/end times with scroll columns instead of typing
25///
26/// # Usage
27///
28/// 1. Bind `Option<DateTimeRange>` through [`TimeRangePickerBind`].
29/// 2. Set [`TimeRangePickerAppearance`] for 12/24-hour format and reference date.
30/// 3. Enable [`DatePickerFeatures::RANGE_PICKERS`] in docs — no runtime license check.
31///
32/// # Best Practices
33///
34/// ## Do's
35///
36/// - Anchor times to a shared `reference_date` when the range is always same-day.
37///
38/// ## Don'ts
39///
40/// - Do not mix timezone binds — keep start and end in the same display zone from [`DatetimeLocale`].
41///
42/// # Examples
43///
44/// ## Time window
45/// Default 12-hour range field with dual time pickers and bind readout.
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="time-range-picker-preview">
54///         <PickerPreviewKnobs />
55///         <TimeRangePicker bind=value />
56///         <div data-testid="time-range-picker-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/// ## 24-hour panels
65/// Scroll-column pickers in 24-hour format.
66/// <!-- preview -->
67/// ```rust
68/// use crate::DateTimeRange;
69/// use crate::preview::PickerPreviewExample;
70/// let value = RwSignal::new(None::<DateTimeRange>);
71/// view! {
72///     <PickerPreviewExample data_testid="TRP-02">
73///         <TimeRangePicker bind=value appearance=TimeRangePickerAppearance::time24() />
74///     </PickerPreviewExample>
75/// }
76/// ```
77#[component_doc(
78    category = "Calendar & Time",
79    preview_slug = "time-range-picker",
80    preview_label = "Time Range Picker",
81    preview_icon = icondata::AiFieldTimeOutlined,
82)]
83#[component]
84pub fn TimeRangePicker(
85    /// Value binding for the combined time range pickers.
86    #[prop(optional, into)]
87    bind: TimeRangePickerBind,
88    /// Time format, reference date, and disabled state.
89    #[prop(optional, into)]
90    appearance: TimeRangePickerAppearance,
91    /// Optional CSS class on the layout wrapper.
92    #[prop(optional, into)]
93    class: MaybeProp<String>,
94) -> impl IntoView {
95    let TimeRangePickerBind { value, id, name } = bind;
96    let TimeRangePickerAppearance {
97        format,
98        reference_date,
99        timezone,
100        disabled,
101    } = appearance;
102
103    let locale = crate::use_datetime_locale();
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    let resolved_reference = Signal::derive(move || reference_date.get());
108
109    let field_bind = TimeRangeFieldBind {
110        value: value_stored.with_value(|v| v.clone()),
111        id,
112        name,
113    };
114    let field_appearance = TimeRangeFieldAppearance {
115        format,
116        reference_date: resolved_reference,
117        timezone,
118        minute_step: Signal::from(1),
119        disabled,
120    };
121
122    let start_picker_appearance = TimePickerAppearance {
123        format,
124        reference_date: Signal::derive(move || Some(reference_date.get())),
125        timezone,
126        disabled,
127    };
128    let end_picker_appearance = TimePickerAppearance {
129        format,
130        reference_date: Signal::derive(move || Some(reference_date.get())),
131        timezone,
132        disabled,
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        let _ = locale.reference_date;
143        parts.join(" ")
144    };
145
146    view! {
147        <style>{picker_style_sheet()}</style>
148        <div class=root_class data-orbital-picker="">
149            <TimeRangeField bind=field_bind appearance=field_appearance />
150            <div class=datetime_range_picker_row_class()>
151                <TimePicker bind=TimePickerBind::new(coordinator.start) appearance=start_picker_appearance />
152                <TimePicker bind=TimePickerBind::new(coordinator.end) appearance=end_picker_appearance />
153            </div>
154        </div>
155    }
156}