orbital_date_pickers/pickers/date_time_picker.rs
1//! [`DateTimePicker`] — combined date and time pickers on one [`OrbitalDateTime`] bind.
2
3use leptos::prelude::*;
4use orbital_core_components::{DatePicker, DatePickerAppearance, TimePicker, TimePickerAppearance};
5use orbital_macros::component_doc;
6use orbital_theme::use_theme_options;
7
8use crate::building_blocks::{DateTimePickerAppearance, DateTimePickerBind};
9use crate::shared::{
10 datetime_picker_root_classes, datetime_picker_row_class, picker_style_sheet,
11 use_datetime_coordinator,
12};
13
14/// Side-by-side date and time pickers sharing one [`OrbitalDateTime`] bind.
15///
16/// DateTimePicker composes core [`DatePicker`](orbital_core_components::DatePicker) and
17/// [`TimePicker`](orbital_core_components::TimePicker). Changing the calendar day preserves the
18/// existing time-of-day. For keyboard segment entry without popovers, use [`DateTimeField`](crate::DateTimeField).
19///
20/// # When to use
21///
22/// - Event scheduling forms (start/end datetime)
23/// - Flows that need both calendar and scroll-column time selection
24/// - Combined date and time selection with calendar and scroll-column pickers
25///
26/// # Usage
27///
28/// 1. Bind `Option<OrbitalDateTime>` via [`DateTimePickerBind`].
29/// 2. Set `appearance.date_format` and `appearance.time_format` for locale masks.
30/// 3. Wrap in [`DatetimeLocale`](crate::DatetimeLocale) for shared defaults.
31///
32/// # Examples
33///
34/// ## Date and time pickers
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-picker-preview">
43/// <PickerPreviewKnobs />
44/// <DateTimePicker bind=value />
45/// <div data-testid="date-time-picker-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/// Selecting date and time updates 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="DTP-02">
59/// <DateTimePicker bind=value />
60/// <div data-testid="DTP-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/// ## 24-hour time
66/// Date picker with 24-hour scroll columns.
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="DTP-03">
74/// <DateTimePicker bind=value appearance=DateTimePickerAppearance::time24() />
75/// </PickerPreviewExample>
76/// }
77/// ```
78///
79/// ## Disabled
80/// Both pickers are non-interactive.
81/// <!-- preview -->
82/// ```rust
83/// use orbital_base_components::OrbitalDateTime;
84/// use crate::preview::PickerPreviewExample;
85/// let value = RwSignal::new(None::<OrbitalDateTime>);
86/// view! {
87/// <PickerPreviewExample data_testid="DTP-04">
88/// <DateTimePicker bind=value appearance=DateTimePickerAppearance { disabled: Signal::from(true), ..Default::default() } />
89/// </PickerPreviewExample>
90/// }
91/// ```
92#[component_doc(
93 category = "Calendar & Time",
94 preview_slug = "date-time-picker",
95 preview_label = "Date Time Picker",
96 preview_icon = icondata::AiCalendarOutlined,
97)]
98#[component]
99pub fn DateTimePicker(
100 /// Value binding for the combined date-time pickers.
101 #[prop(optional, into)]
102 bind: DateTimePickerBind,
103 /// Date format, time format, timezone, and disabled state.
104 #[prop(optional, into)]
105 appearance: DateTimePickerAppearance,
106 /// Optional CSS class on the layout wrapper.
107 #[prop(optional, into)]
108 class: MaybeProp<String>,
109) -> impl IntoView {
110 let DateTimePickerBind { value, id, name } = bind;
111 let DateTimePickerAppearance {
112 date_format,
113 time_format,
114 timezone,
115 disabled,
116 placement,
117 } = appearance;
118
119 let locale = crate::use_datetime_locale();
120 let theme_options = use_theme_options();
121 let fallback_reference = Signal::derive(move || locale.reference_date);
122 let coordinator = use_datetime_coordinator(value, id, name, fallback_reference);
123
124 let date_appearance = DatePickerAppearance {
125 format: date_format,
126 timezone,
127 disabled,
128 placement,
129 ..Default::default()
130 };
131 let time_appearance = TimePickerAppearance {
132 format: time_format,
133 reference_date: coordinator.reference_date,
134 timezone,
135 disabled,
136 };
137
138 let root_class = move || {
139 let mut parts = vec![datetime_picker_root_classes(theme_options.get().density)];
140 if let Some(extra) = class.get() {
141 if !extra.is_empty() {
142 parts.push(extra);
143 }
144 }
145 parts.join(" ")
146 };
147
148 view! {
149 <style>{picker_style_sheet()}</style>
150 <div class=root_class data-orbital-picker="">
151 <div class=datetime_picker_row_class()>
152 <DatePicker bind=coordinator.date_bind appearance=date_appearance />
153 <TimePicker bind=coordinator.time_bind appearance=time_appearance />
154 </div>
155 </div>
156 }
157}