orbital_date_pickers/building_blocks/time_field.rs
1//! [`TimeField`] — segmented time input bound to [`OrbitalDateTime`].
2
3use leptos::prelude::*;
4use orbital_macros::component_doc;
5use orbital_theme::use_theme_options;
6
7use crate::shared::picker_style_sheet;
8use crate::{use_datetime_locale, SegmentedDatetimeField};
9
10use super::field_types::{TimeFieldAppearance, TimeFieldBind};
11
12/// Segmented time input bound to [`OrbitalDateTime`] on a reference calendar day.
13///
14/// TimeField renders hour/minute (and meridiem for 12-hour) segments. Parsed values are
15/// anchored to `appearance.reference_date` or the nearest [`DatetimeLocale`](crate::DatetimeLocale)
16/// default. For scroll-column time selection, use core [`TimePicker`](orbital_core_components::TimePicker).
17///
18/// # When to use
19///
20/// - Time-of-day entry with keyboard-friendly segments
21/// - Scheduling forms paired with a date field on the same reference day
22///
23/// # Usage
24///
25/// 1. Bind `Option<OrbitalDateTime>` via [`TimeFieldBind`].
26/// 2. Set `appearance.format` to `Time12` or `Time24`.
27/// 3. Set `appearance.timezone` and `reference_date` when the anchor day or zone differs from defaults.
28///
29/// # Lifecycle
30///
31/// - **Value:** completed time segments commit on blur via [`SegmentedDatetimeField`].
32/// - **Open state:** no popover — segments are always editable inline.
33///
34/// # Timezone
35///
36/// `appearance.timezone` controls how hour/minute segments resolve to [`OrbitalDateTime`]
37/// on `appearance.reference_date`.
38///
39/// # Examples
40///
41/// ## Time segments
42/// Default 12-hour segmented input with bind readout for E2E.
43/// <!-- preview -->
44/// ```rust
45/// use orbital_base_components::{OrbitalDateTime, ToUnixSeconds};
46/// use crate::preview::{PickerPreviewExample, PickerPreviewKnobs};
47/// let value = RwSignal::new(None::<OrbitalDateTime>);
48/// view! {
49/// <PickerPreviewExample data_testid="time-field-preview">
50/// <PickerPreviewKnobs />
51/// <TimeField bind=value />
52/// <div data-testid="time-field-preview-VALUE">{move || value.get().map(|v| v.to_unix_seconds().to_string()).unwrap_or_else(|| "none".to_string())}</div>
53/// </PickerPreviewExample>
54/// }
55/// ```
56///
57/// ## 24-hour format
58/// Hour and minute segments without meridiem.
59/// <!-- preview -->
60/// ```rust
61/// use orbital_base_components::OrbitalDateTime;
62/// use crate::preview::PickerPreviewExample;
63/// let value = RwSignal::new(None::<OrbitalDateTime>);
64/// view! {
65/// <PickerPreviewExample data_testid="TF-02">
66/// <TimeField bind=value appearance=TimeFieldAppearance::time24() />
67/// </PickerPreviewExample>
68/// }
69/// ```
70///
71/// ## Bind readout
72/// Completed segments update the bound value readout.
73/// <!-- preview -->
74/// ```rust
75/// use orbital_base_components::{OrbitalDateTime, ToUnixSeconds};
76/// use crate::preview::PickerPreviewExample;
77/// let value = RwSignal::new(None::<OrbitalDateTime>);
78/// view! {
79/// <PickerPreviewExample data_testid="TF-03">
80/// <TimeField bind=value />
81/// <div data-testid="TF-03-VALUE">{move || value.get().map(|v| v.to_unix_seconds().to_string()).unwrap_or_else(|| "none".to_string())}</div>
82/// </PickerPreviewExample>
83/// }
84/// ```
85#[component_doc(
86 category = "Calendar & Time",
87 preview_slug = "time-field",
88 preview_label = "Time Field",
89 preview_icon = icondata::AiFieldTimeOutlined,
90)]
91#[component]
92pub fn TimeField(
93 /// Value binding for the segmented time input.
94 #[prop(optional, into)]
95 bind: TimeFieldBind,
96 /// Format, reference day, and disabled state.
97 #[prop(optional, into)]
98 appearance: TimeFieldAppearance,
99 /// Optional CSS class on the layout wrapper.
100 #[prop(optional, into)]
101 class: MaybeProp<String>,
102) -> impl IntoView {
103 let TimeFieldBind { value, id, name } = bind;
104 let TimeFieldAppearance {
105 format,
106 reference_date,
107 timezone,
108 disabled,
109 minute_step: _,
110 } = appearance;
111
112 let _locale = use_datetime_locale();
113 let theme_options = use_theme_options();
114 let resolved_format = Signal::derive(move || format.get());
115 let resolved_timezone = Signal::derive(move || timezone.get());
116 let resolved_reference = Signal::derive(move || reference_date.get().start_of_day());
117
118 let root_class = move || {
119 let mut parts = vec!["orb-picker-segmented-field-host".to_string()];
120 if let Some(extra) = class.get() {
121 if !extra.is_empty() {
122 parts.push(extra);
123 }
124 }
125 let _ = theme_options.get();
126 parts.join(" ")
127 };
128
129 view! {
130 <style>{picker_style_sheet()}</style>
131 <div class=root_class data-orbital-picker="">
132 <SegmentedDatetimeField
133 value=value
134 format=resolved_format
135 timezone=resolved_timezone
136 reference_date=resolved_reference
137 disabled=disabled
138 testid_prefix="time-field"
139 is_time=true
140 id=id
141 name=name
142 />
143 </div>
144 }
145}