orbital_date_pickers/building_blocks/date_time_field.rs
1//! [`DateTimeField`] — combined date and time segmented input.
2
3use leptos::prelude::*;
4use orbital_base_components::DatetimeFormat;
5use orbital_macros::component_doc;
6use orbital_theme::use_theme_options;
7
8use super::datetime_field_coordinator::use_datetime_field_coordinator;
9use crate::building_blocks::{
10 DateField, DateFieldAppearance, DateTimeFieldAppearance, DateTimeFieldBind, TimeField,
11 TimeFieldAppearance,
12};
13use crate::shared::{datetime_field_row_class, picker_style_sheet};
14
15/// Combined date and time segmented input bound to [`OrbitalDateTime`].
16///
17/// DateTimeField composes [`DateField`](crate::DateField) and [`TimeField`](crate::TimeField) on
18/// one shared bind. Changing the date preserves the existing time-of-day. For popover pickers,
19/// use [`DateTimePicker`](crate::DateTimePicker) instead.
20///
21/// # When to use
22///
23/// - Dense forms needing keyboard-friendly date and time entry
24/// - Flows that prefer segmented input over scroll-column pickers
25///
26/// # Usage
27///
28/// 1. Bind `Option<OrbitalDateTime>` via [`DateTimeFieldBind`].
29/// 2. Set `appearance.date_format` and `appearance.time_format`.
30/// 3. Wrap in [`Field`](orbital_core_components::Field) when a visible label is required.
31///
32/// # Examples
33///
34/// ## Combined segments
35/// Default US date + 12-hour time with bind readout for E2E.
36/// <!-- preview -->
37/// ```rust
38/// use orbital_base_components::{OrbitalDateTime, ToUnixSeconds};
39/// use crate::preview::{PickerPreviewExample, PickerPreviewKnobs};
40/// let value = RwSignal::new(None::<OrbitalDateTime>);
41/// view! {
42/// <PickerPreviewExample data_testid="date-time-field-preview">
43/// <PickerPreviewKnobs />
44/// <DateTimeField bind=value />
45/// <div data-testid="date-time-field-preview-VALUE">{move || value.get().map(|v| v.to_unix_seconds().to_string()).unwrap_or_else(|| "none".to_string())}</div>
46/// </PickerPreviewExample>
47/// }
48/// ```
49///
50/// ## Bind readout
51/// Completed segments update the bound [`OrbitalDateTime`].
52/// <!-- preview -->
53/// ```rust
54/// use orbital_base_components::{OrbitalDateTime, ToUnixSeconds};
55/// use crate::preview::PickerPreviewExample;
56/// let value = RwSignal::new(None::<OrbitalDateTime>);
57/// view! {
58/// <PickerPreviewExample data_testid="DTF-02">
59/// <DateTimeField bind=value />
60/// <div data-testid="DTF-02-VALUE">{move || value.get().map(|v| v.to_unix_seconds().to_string()).unwrap_or_else(|| "none".to_string())}</div>
61/// </PickerPreviewExample>
62/// }
63/// ```
64///
65/// ## ISO date and 24-hour time
66/// Year-month-day segments followed by 24-hour hour/minute.
67/// <!-- preview -->
68/// ```rust
69/// use orbital_base_components::OrbitalDateTime;
70/// use crate::preview::PickerPreviewExample;
71/// let value = RwSignal::new(None::<OrbitalDateTime>);
72/// view! {
73/// <PickerPreviewExample data_testid="DTF-03">
74/// <DateTimeField bind=value appearance=DateTimeFieldAppearance::iso_time24() />
75/// </PickerPreviewExample>
76/// }
77/// ```
78#[component_doc(
79 category = "Calendar & Time",
80 preview_slug = "date-time-field",
81 preview_label = "Date Time Field",
82 preview_icon = icondata::AiFieldTimeOutlined,
83)]
84#[component]
85pub fn DateTimeField(
86 /// Value binding for the combined segmented input.
87 #[prop(optional, into)]
88 bind: DateTimeFieldBind,
89 /// Date format, time format, timezone, and disabled state.
90 #[prop(optional, into)]
91 appearance: DateTimeFieldAppearance,
92 /// Optional CSS class on the layout wrapper.
93 #[prop(optional, into)]
94 class: MaybeProp<String>,
95) -> impl IntoView {
96 let DateTimeFieldBind { value, id, name } = bind;
97 let DateTimeFieldAppearance {
98 date_format,
99 time_format,
100 timezone,
101 disabled,
102 } = appearance;
103
104 let locale = crate::use_datetime_locale();
105 let theme_options = use_theme_options();
106 let fallback_reference = Signal::derive(move || locale.reference_date);
107 let coordinator = use_datetime_field_coordinator(value, id, name, fallback_reference);
108
109 let resolved_date_format = Signal::derive(move || match date_format.get() {
110 DatetimeFormat::Time24 | DatetimeFormat::Time12 => locale.default_format,
111 other => other,
112 });
113
114 let date_appearance = DateFieldAppearance {
115 format: resolved_date_format,
116 timezone,
117 disabled,
118 };
119 let time_appearance = TimeFieldAppearance {
120 format: time_format,
121 reference_date: coordinator.reference_date,
122 timezone,
123 disabled,
124 ..Default::default()
125 };
126
127 let root_class = move || {
128 let mut parts = Vec::new();
129 if let Some(extra) = class.get() {
130 if !extra.is_empty() {
131 parts.push(extra);
132 }
133 }
134 let _ = theme_options.get();
135 parts.join(" ")
136 };
137
138 view! {
139 <style>{picker_style_sheet()}</style>
140 <div class=root_class data-orbital-picker="">
141 <div class=datetime_field_row_class()>
142 <DateField bind=coordinator.date_bind appearance=date_appearance />
143 <TimeField bind=coordinator.time_bind appearance=time_appearance />
144 </div>
145 </div>
146 }
147}