orbital_date_pickers/pickers/date_range_picker/
mod.rs1mod styles;
4
5use leptos::prelude::*;
6use orbital_base_components::OverlayDismiss;
7use orbital_core_components::{
8 Icon, Popover, PopoverPosition, PopoverSize, PopoverTrigger, PopoverTriggerType,
9};
10use orbital_macros::component_doc;
11use orbital_theme::use_theme_options;
12
13use crate::building_blocks::{
14 DateRangeCalendar, DateRangeCalendarAppearance, DateRangeCalendarBind, DateRangeField,
15 DateRangeFieldAppearance, DateRangeFieldBind, DateRangePickerAppearance, DateRangePickerBind,
16};
17use crate::shared::{
18 layout_root_classes, picker_style_sheet, DateRangePickerSlots, OpenTriggerSlot,
19 PickerFieldContext, PickerFieldSlot, PickerFieldSlotHost,
20};
21use styles::date_range_picker_styles;
22
23#[component_doc(
91 category = "Calendar & Time",
92 preview_slug = "date-range-picker",
93 preview_label = "Date Range Picker",
94 preview_icon = icondata::AiCalendarOutlined,
95)]
96#[component]
97pub fn DateRangePicker(
98 #[prop(optional, into)]
100 bind: DateRangePickerBind,
101 #[prop(optional, into)]
103 appearance: DateRangePickerAppearance,
104 #[prop(optional, into)]
106 class: MaybeProp<String>,
107 #[prop(optional)]
109 picker_field_slot: Option<PickerFieldSlot>,
110 #[prop(optional)]
112 open_trigger_slot: Option<OpenTriggerSlot>,
113) -> impl IntoView {
114 let DateRangePickerBind { value, id, name } = bind;
115 let DateRangePickerAppearance {
116 format,
117 timezone,
118 disabled,
119 calendars,
120 close_on_select,
121 placement,
122 } = appearance;
123
124 let theme_options = use_theme_options();
125 let value_stored = StoredValue::new(value);
126
127 let slots = DateRangePickerSlots::from_slot_props(picker_field_slot, open_trigger_slot);
128 let field_children = StoredValue::new(slots.field.map(|slot| slot.children));
129 let open_trigger_children = StoredValue::new(slots.open_trigger.map(|slot| slot.children));
130
131 let field_context = Signal::derive(move || PickerFieldContext {
132 value: Signal::derive(move || value_stored.with_value(|v| v.get())),
133 disabled,
134 format,
135 timezone,
136 });
137 let calendar_appearance = DateRangeCalendarAppearance {
138 timezone,
139 min_date: Signal::from(None),
140 max_date: Signal::from(None),
141 disabled,
142 calendars,
143 day: None,
144 };
145
146 let root_class = move || {
147 let mut parts = vec![layout_root_classes(theme_options.get().density)];
148 if let Some(extra) = class.get() {
149 if !extra.is_empty() {
150 parts.push(extra);
151 }
152 }
153 parts.join(" ")
154 };
155
156 view! {
157 <style>{date_range_picker_styles()}</style>
158 <style>{picker_style_sheet()}</style>
159 <div class=root_class data-orbital-picker="">
160 <Popover
161 trigger_type=PopoverTriggerType::Click
162 position=placement_to_popover_position(placement.get_untracked())
163 size=Signal::from(PopoverSize::Large)
164 >
165 <PopoverTrigger slot>
166 <div class="orb-picker-range-picker__trigger">
167 {move || {
168 if let Some(children) = field_children.get_value() {
169 let children = children.clone();
170 view! {
171 <PickerFieldSlotHost context=field_context children=children />
172 }
173 .into_any()
174 } else {
175 view! {
176 <DateRangeField
177 bind=DateRangeFieldBind {
178 value: value_stored.with_value(|v| v.clone()),
179 id,
180 name,
181 }
182 appearance=DateRangeFieldAppearance { format, timezone, disabled }
183 />
184 }
185 .into_any()
186 }
187 }}
188 {move || {
189 if let Some(children) = open_trigger_children.get_value() {
190 let children = children.clone();
191 children().into_any()
192 } else {
193 view! {
194 <button
195 type="button"
196 class="orb-picker-range-picker__open-btn"
197 aria-label="Open calendar"
198 disabled=move || disabled.get()
199 >
200 <Icon icon=icondata::AiCalendarOutlined />
201 </button>
202 }
203 .into_any()
204 }
205 }}
206 </div>
207 </PopoverTrigger>
208 <DateRangePickerPanel
209 value=value_stored
210 appearance=calendar_appearance
211 close_on_select=close_on_select
212 />
213 </Popover>
214 </div>
215 }
216}
217
218#[component]
219fn DateRangePickerPanel(
220 value: StoredValue<orbital_base_components::OptionBind<crate::DateTimeRange>>,
221 appearance: DateRangeCalendarAppearance,
222 close_on_select: Signal<bool>,
223) -> impl IntoView {
224 let dismiss = use_context::<OverlayDismiss>();
225 let prev_complete = RwSignal::new(None::<bool>);
226
227 Effect::new(move |_| {
228 if !close_on_select.get() {
229 return;
230 }
231 let complete = value.with_value(|v| v.get()).is_some();
232 if complete && prev_complete.get_untracked() == Some(false) {
233 if let Some(dismiss) = dismiss {
234 dismiss.close.run(());
235 }
236 }
237 prev_complete.set(Some(complete));
238 });
239
240 view! {
241 <div class="orb-picker-range-picker__panel">
242 <DateRangeCalendar
243 bind=DateRangeCalendarBind {
244 value: value.with_value(|v| v.clone()),
245 }
246 appearance=appearance
247 />
248 </div>
249 }
250}
251
252fn placement_to_popover_position(placement: orbital_base_components::Placement) -> PopoverPosition {
253 use orbital_base_components::Placement;
254 match placement {
255 Placement::Top => PopoverPosition::Top,
256 Placement::Bottom => PopoverPosition::Bottom,
257 Placement::Left => PopoverPosition::Left,
258 Placement::Right => PopoverPosition::Right,
259 Placement::TopStart => PopoverPosition::TopStart,
260 Placement::TopEnd => PopoverPosition::TopEnd,
261 Placement::LeftStart => PopoverPosition::LeftStart,
262 Placement::LeftEnd => PopoverPosition::LeftEnd,
263 Placement::RightStart => PopoverPosition::RightStart,
264 Placement::RightEnd => PopoverPosition::RightEnd,
265 Placement::BottomStart => PopoverPosition::BottomStart,
266 Placement::BottomEnd => PopoverPosition::BottomEnd,
267 }
268}