Skip to main content

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}