Skip to main content

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}