Skip to main content

orbital_date_pickers/guides/
custom_field.rs

1//! Custom field slot with picker context (DP-31).
2
3use leptos::prelude::*;
4use orbital_core_components::{Field, Input, InputAppearance, InputBind};
5use orbital_macros::component_doc;
6
7use crate::use_picker_field;
8use orbital_base_components::{format_datetime, DatetimeFormat};
9
10#[component]
11pub fn CustomRangeSummaryField() -> impl IntoView {
12    let ctx = use_picker_field();
13    let text = RwSignal::new(String::new());
14    Effect::new(move |_| {
15        let display = ctx
16            .value
17            .get()
18            .map(|range| {
19                format!(
20                    "{} – {}",
21                    format_datetime(range.start, DatetimeFormat::IsoDate),
22                    format_datetime(range.end, DatetimeFormat::IsoDate)
23                )
24            })
25            .unwrap_or_else(|| "Select a date range".to_string());
26        text.set(display);
27    });
28
29    view! {
30        <Field label="Travel dates" name="travel_range">
31            <Input
32                bind=InputBind { value: text.into(), ..Default::default() }
33                appearance=InputAppearance {
34                    readonly: Signal::from(true),
35                    disabled: ctx.disabled,
36                    ..Default::default()
37                }
38            />
39        </Field>
40    }
41}
42
43/// Replace the default segmented field with a custom control via [`use_picker_field`].
44///
45/// [`PickerFieldSlot`](crate::PickerFieldSlot) swaps the built-in range input for any child
46/// component that reads picker context through [`use_picker_field`]. The hook exposes the
47/// bound value, disabled state, and format signals so custom summaries stay in sync with
48/// the calendar popover.
49///
50/// # When to use
51///
52/// - Read-only summary inputs that open a calendar on focus or click elsewhere
53/// - Branded range displays (for example "Jan 4 – Jan 10" in a single line)
54/// - Flows where the default segmented mask does not match your design system
55///
56/// # Usage
57///
58/// 1. Implement a field component that calls [`use_picker_field`] inside a [`PickerFieldSlot`](crate::PickerFieldSlot) child.
59/// 2. Mirror `ctx.value` and `ctx.disabled` into your control (typically a readonly [`Input`](orbital_core_components::Input)).
60/// 3. Pass the slot as a child of [`DateRangePicker`](crate::DateRangePicker) or other range pickers that support field replacement.
61///
62/// # Best Practices
63///
64/// ## Do's
65///
66/// * Keep the custom field readonly when the calendar owns editing
67/// * Format display text with [`format_datetime`] using the range endpoints' timezones
68/// * Wrap custom inputs in [`Field`] for labeling and validation
69///
70/// ## Don'ts
71///
72/// * Do not call [`use_picker_field`] outside a picker field slot — context will be missing
73/// * Do not write directly to the bind signal from the summary without updating the calendar selection
74///
75/// # Examples
76///
77/// ## Custom read-only summary
78/// Replace the default segmented range field with a summary input bound through picker context.
79/// <!-- preview -->
80/// ```rust
81/// use crate::preview::{PickerPreviewExample, PickerPreviewKnobs};
82/// use crate::{CustomRangeSummaryField, DateRangePicker, DateTimeRange, PickerFieldSlot};
83/// let value = RwSignal::new(None::<DateTimeRange>);
84/// view! {
85///     <PickerPreviewExample data_testid="date-pickers-custom-field-preview">
86///         <PickerPreviewKnobs />
87///         <DateRangePicker bind=value>
88///             <PickerFieldSlot slot>
89///                 <CustomRangeSummaryField />
90///             </PickerFieldSlot>
91///         </DateRangePicker>
92///     </PickerPreviewExample>
93/// }
94/// ```
95#[component_doc(
96    category = "Calendar & Time",
97    preview_slug = "date-pickers-custom-field",
98    preview_label = "Custom Field",
99    preview_icon = icondata::AiFormOutlined,
100)]
101#[component]
102pub fn DatePickersCustomFieldGuide() -> impl IntoView {
103    view! { () }
104}