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}