Skip to main content

orbital_date_pickers/building_blocks/
date_field.rs

1//! [`DateField`] — segmented date input bound to [`OrbitalDateTime`].
2
3use leptos::prelude::*;
4use orbital_base_components::DatetimeFormat;
5use orbital_macros::component_doc;
6use orbital_theme::use_theme_options;
7
8use crate::shared::picker_style_sheet;
9use crate::{use_datetime_locale, SegmentedDatetimeField};
10
11use super::field_types::{DateFieldAppearance, DateFieldBind};
12
13/// Segmented date input with locale-aware section masks, bound to [`OrbitalDateTime`].
14///
15/// DateField renders editable month/day/year (or ISO) segments instead of a single text box.
16/// Values are stored as start-of-day [`OrbitalDateTime`] in the chosen timezone. Convert at
17/// API boundaries via `ToUnixSeconds` or `ToIso8601`. For a text field with popover calendar,
18/// use core [`DatePicker`](orbital_core_components::DatePicker) instead.
19///
20/// # When to use
21///
22/// - Dense forms that benefit from section-wise date entry
23/// - Keyboards-first flows where users tab through date parts
24/// - Flows that need segmented date entry without a popover calendar
25///
26/// # Usage
27///
28/// 1. Bind `Option<OrbitalDateTime>` via [`DateFieldBind`].
29/// 2. Set `appearance.format` to `IsoDate` or `UsDate`.
30/// 3. Wrap in [`Field`](orbital_core_components::Field) when a visible label is required.
31///
32/// # Lifecycle
33///
34/// - **Value:** each segment commits the parsed date on blur when all required sections are complete.
35/// - **Open state:** no popover — segments are always editable inline.
36///
37/// # Timezone
38///
39/// `appearance.timezone` controls start-of-day normalization for parsed segment values.
40///
41/// # Best Practices
42///
43/// ## Do's
44///
45/// * Bind with [`OrbitalDateTime`], not raw unix seconds
46/// * Wrap preview examples in a native element with `data-testid`
47///
48/// ## Don'ts
49///
50/// * Do not use for time-of-day — prefer [`TimeField`](crate::TimeField)
51/// * Do not put `data-testid` on the component itself
52///
53/// # Examples
54///
55/// ## Segmented input
56/// Default US-format month/day/year segments with bind readout for E2E.
57/// <!-- preview -->
58/// ```rust
59/// use orbital_base_components::{OrbitalDateTime, ToUnixSeconds};
60/// use crate::preview::{PickerPreviewExample, PickerPreviewKnobs};
61/// let value = RwSignal::new(None::<OrbitalDateTime>);
62/// view! {
63///     <PickerPreviewExample data_testid="date-field-preview">
64///         <PickerPreviewKnobs />
65///         <DateField bind=value />
66///         <div data-testid="date-field-preview-VALUE">{move || value.get().map(|v| v.to_unix_seconds().to_string()).unwrap_or_else(|| "none".to_string())}</div>
67///     </PickerPreviewExample>
68/// }
69/// ```
70///
71/// ## Bind readout
72/// Typing complete segments updates the bound [`OrbitalDateTime`].
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="DF-02">
80///         <DateField bind=value />
81///         <div data-testid="DF-02-VALUE">{move || value.get().map(|v| v.to_unix_seconds().to_string()).unwrap_or_else(|| "none".to_string())}</div>
82///     </PickerPreviewExample>
83/// }
84/// ```
85///
86/// ## ISO format
87/// Year-month-day segment order for ISO locales.
88/// <!-- preview -->
89/// ```rust
90/// use orbital_base_components::{DatetimeFormat, OrbitalDateTime};
91/// use crate::preview::PickerPreviewExample;
92/// let value = RwSignal::new(None::<OrbitalDateTime>);
93/// view! {
94///     <PickerPreviewExample data_testid="DF-03">
95///         <DateField bind=value appearance=DateFieldAppearance { format: Signal::from(DatetimeFormat::IsoDate), ..Default::default() } />
96///     </PickerPreviewExample>
97/// }
98/// ```
99#[component_doc(
100    category = "Calendar & Time",
101    preview_slug = "date-field",
102    preview_label = "Date Field",
103    preview_icon = icondata::AiFieldBinaryOutlined,
104)]
105#[component]
106pub fn DateField(
107    /// Value binding for the segmented date input.
108    #[prop(optional, into)]
109    bind: DateFieldBind,
110    /// Format, timezone, and disabled state.
111    #[prop(optional, into)]
112    appearance: DateFieldAppearance,
113    /// Optional CSS class on the layout wrapper.
114    #[prop(optional, into)]
115    class: MaybeProp<String>,
116) -> impl IntoView {
117    let DateFieldBind { value, id, name } = bind;
118    let DateFieldAppearance {
119        format,
120        timezone,
121        disabled,
122    } = appearance;
123
124    let locale = use_datetime_locale();
125    let theme_options = use_theme_options();
126    let resolved_format = Signal::derive(move || match format.get() {
127        DatetimeFormat::Time24 | DatetimeFormat::Time12 => locale.default_format,
128        other => other,
129    });
130    let resolved_timezone = Signal::derive(move || timezone.get());
131    let reference_date = Signal::derive(move || locale.reference_date);
132
133    let root_class = move || {
134        let mut parts = vec!["orb-picker-segmented-field-host".to_string()];
135        if let Some(extra) = class.get() {
136            if !extra.is_empty() {
137                parts.push(extra);
138            }
139        }
140        let _ = theme_options.get();
141        parts.join(" ")
142    };
143
144    view! {
145        <style>{picker_style_sheet()}</style>
146        <div class=root_class data-orbital-picker="">
147            <SegmentedDatetimeField
148                value=value
149                format=resolved_format
150                timezone=resolved_timezone
151                reference_date=reference_date
152                disabled=disabled
153                testid_prefix="date-field"
154                id=id
155                name=name
156            />
157        </div>
158    }
159}