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}