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}