Skip to main content

euv_ui/component/date_picker/view/
fn.rs

1use super::*;
2
3/// The number of cells in the day grid: six weeks of seven days.
4///
5/// The grid always has this shape so the panel does not change height when
6/// the user pages from a five-week month into a six-week one.
7const GRID_CELLS: u32 = 42;
8
9/// The number of days in a second.
10#[cfg(not(target_arch = "wasm32"))]
11const SECONDS_PER_DAY: i64 = 86_400;
12
13/// The glyph drawn on the previous-month chevron.
14const PREVIOUS_MONTH_GLYPH: &str = "\u{2039}";
15
16/// The glyph drawn on the next-month chevron.
17const NEXT_MONTH_GLYPH: &str = "\u{203a}";
18
19/// The glyph drawn on the trailing edge of the trigger.
20const CARET_GLYPH: &str = "\u{25be}";
21
22/// The label of the button that selects today's date.
23const TODAY_LABEL: &str = "Today";
24
25/// The label of the button that empties the field.
26const CLEAR_LABEL: &str = "Clear";
27
28/// Converts a civil date into the number of days since the Unix epoch.
29///
30/// The algorithm is Howard Hinnant's `days_from_civil`, which is exact for
31/// the proleptic Gregorian calendar and needs no leap-year special cases.
32///
33/// # Arguments
34///
35/// - `i64` - The year.
36/// - `u32` - The month, `1` through `12`.
37///
38/// # Returns
39///
40/// - `i64` - The number of days between the epoch and the given date.
41pub fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
42    let shifted_year: i64 = if month <= 2 { year - 1 } else { year };
43    let era: i64 = if shifted_year >= 0 {
44        shifted_year
45    } else {
46        shifted_year - 399
47    } / 400;
48    let year_of_era: i64 = shifted_year - era * 400;
49    let shifted_month: i64 = if month > 2 {
50        month as i64 - 3
51    } else {
52        month as i64 + 9
53    };
54    let day_of_year: i64 = (153 * shifted_month + 2) / 5 + day as i64 - 1;
55    let day_of_era: i64 = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
56    era * 146_097 + day_of_era - 719_468
57}
58
59/// Converts a number of days since the Unix epoch into a civil date.
60///
61/// The algorithm is Howard Hinnant's `civil_from_days`, the exact inverse of
62/// [`days_from_civil`].
63///
64/// # Arguments
65///
66/// - `i64` - The number of days between the epoch and the wanted date.
67///
68/// # Returns
69///
70/// - `(i64, u32, u32)` - The year, the month and the day of the month.
71pub fn civil_from_days(days: i64) -> (i64, u32, u32) {
72    let shifted: i64 = days + 719_468;
73    let era: i64 = if shifted >= 0 {
74        shifted
75    } else {
76        shifted - 146_096
77    } / 146_097;
78    let day_of_era: i64 = shifted - era * 146_097;
79    let year_of_era: i64 =
80        (day_of_era - day_of_era / 1460 + day_of_era / 36524 - day_of_era / 146_096) / 365;
81    let year: i64 = year_of_era + era * 400;
82    let day_of_year: i64 = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
83    let shifted_month: i64 = (5 * day_of_year + 2) / 153;
84    let day: u32 = (day_of_year - (153 * shifted_month + 2) / 5 + 1) as u32;
85    let month: u32 = if shifted_month < 10 {
86        shifted_month + 3
87    } else {
88        shifted_month - 9
89    } as u32;
90    (if month <= 2 { year + 1 } else { year }, month, day)
91}
92
93/// Returns the weekday of a date, counting Monday as `0`.
94///
95/// The epoch itself was a Thursday, which is index `3` in a Monday-first
96/// week.
97///
98/// # Arguments
99///
100/// - `i64` - The year.
101/// - `u32` - The month, `1` through `12`.
102///
103/// # Returns
104///
105/// - `u32` - The weekday index, `0` for Monday through `6` for Sunday.
106pub fn weekday_index(year: i64, month: u32, day: u32) -> u32 {
107    ((days_from_civil(year, month, day) + 3).rem_euclid(7)) as u32
108}
109
110/// Returns whether a year is a leap year in the Gregorian calendar.
111///
112/// # Arguments
113///
114/// - `i64` - The year.
115///
116/// # Returns
117///
118/// - `bool` - `true` when the year has a 29th of February.
119pub fn is_leap_year(year: i64) -> bool {
120    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
121}
122
123/// Returns the number of days in a month.
124///
125/// # Arguments
126///
127/// - `i64` - The year.
128/// - `u32` - The month, `1` through `12`.
129///
130/// # Returns
131///
132/// - `u32` - The length of the month, or `0` for a month outside the year.
133pub fn days_in_month(year: i64, month: u32) -> u32 {
134    match month {
135        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
136        4 | 6 | 9 | 11 => 30,
137        2 => {
138            if is_leap_year(year) {
139                29
140            } else {
141                28
142            }
143        }
144        _ => 0,
145    }
146}
147
148/// Returns the month that follows or precedes the given one.
149///
150/// # Arguments
151///
152/// - `i64` - The year of the month.
153/// - `u32` - The month, `1` through `12`.
154///
155/// # Returns
156///
157/// - `(i64, u32)` - The year and the month that was walked to.
158pub fn adjacent_month(year: i64, month: u32, delta: i64) -> (i64, u32) {
159    let index: i64 = year * 12 + month as i64 - 1 + delta;
160    (index.div_euclid(12), (index.rem_euclid(12) + 1) as u32)
161}
162
163/// Encodes a year and a month into a single number the month signal holds.
164///
165/// # Arguments
166///
167/// - `i64` - The year.
168/// - `u32` - The month, `1` through `12`.
169///
170/// # Returns
171///
172/// - `i64` - The encoded month.
173pub fn encode_month(year: i64, month: u32) -> i64 {
174    year * 12 + month as i64 - 1
175}
176
177/// Decodes a month signal back into a year and a month.
178///
179/// # Arguments
180///
181/// - `i64` - The encoded month.
182///
183/// # Returns
184///
185/// - `(i64, u32)` - The year and the month.
186pub fn decode_month(code: i64) -> (i64, u32) {
187    (code.div_euclid(12), (code.rem_euclid(12) + 1) as u32)
188}
189
190/// Formats a date as `YYYY-MM-DD`.
191///
192/// # Arguments
193///
194/// - `i64` - The year.
195/// - `u32` - The month, `1` through `12`.
196///
197/// # Returns
198///
199/// - `String` - The formatted date.
200pub fn format_date(year: i64, month: u32, day: u32) -> String {
201    format!("{year:04}-{month:02}-{day:02}")
202}
203
204/// Parses a `YYYY-MM-DD` date.
205///
206/// A date that does not have that shape, or that names a day the month does
207/// not have, is rejected rather than silently repaired.
208///
209/// # Arguments
210///
211/// - `&str` - The text to parse.
212///
213/// # Returns
214///
215/// - `Option<(i64, u32, u32)>` - The year, the month and the day, or `None`
216///   when the text is not a valid date.
217pub fn parse_date(text: &str) -> Option<(i64, u32, u32)> {
218    let trimmed: &str = text.trim();
219    if trimmed.is_empty() {
220        return None;
221    }
222    let mut parts = trimmed.split('-');
223    let year_text: &str = parts.next()?;
224    let month_text: &str = parts.next()?;
225    let day_text: &str = parts.next()?;
226    if parts.next().is_some() {
227        return None;
228    }
229    let year: i64 = year_text.parse().ok()?;
230    let month: u32 = month_text.parse().ok()?;
231    let day: u32 = day_text.parse().ok()?;
232    if !(1..=12).contains(&month) {
233        return None;
234    }
235    if day < 1 || day > days_in_month(year, month) {
236        return None;
237    }
238    Some((year, month, day))
239}
240
241/// Returns whether a date sits inside the inclusive bounds.
242///
243/// An empty or unparsable bound means that side is not bounded at all.
244///
245/// # Arguments
246///
247/// - `(i64, u32, u32)` - The date under test.
248/// - `&str` - The earliest selectable date, formatted as `YYYY-MM-DD`.
249///
250/// # Returns
251///
252/// - `bool` - `true` when the date may be selected.
253pub fn date_within_bounds(date: (i64, u32, u32), min: &str, max: &str) -> bool {
254    if let Some(minimum) = parse_date(min)
255        && date < minimum
256    {
257        return false;
258    }
259    if let Some(maximum) = parse_date(max)
260        && date > maximum
261    {
262        return false;
263    }
264    true
265}
266
267/// Builds the day grid of a month.
268///
269/// The grid always holds [`GRID_CELLS`] cells: the days of the month in the
270/// middle, the trailing days of the previous month in front of them and the
271/// leading days of the next month behind them. The spill-over cells are
272/// marked as muted so the caller can tell them apart.
273///
274/// # Arguments
275///
276/// - `i64` - The year.
277/// - `u32` - The month, `1` through `12`.
278/// - `(i64, u32, u32)` - The current date, used to mark today's cell.
279///
280/// # Returns
281///
282/// - `Vec<EuvCalendarDay>` - The cells of the grid, in display order.
283pub fn month_grid(year: i64, month: u32, today: (i64, u32, u32)) -> Vec<EuvCalendarDay> {
284    let leading: u32 = weekday_index(year, month, 1);
285    let month_days: u32 = days_in_month(year, month);
286    let trailing: u32 = GRID_CELLS - leading - month_days;
287    let (previous_year, previous_month) = adjacent_month(year, month, -1);
288    let previous_days: u32 = days_in_month(previous_year, previous_month);
289    let (next_year, next_month) = adjacent_month(year, month, 1);
290    let mut cells: Vec<EuvCalendarDay> = Vec::with_capacity(GRID_CELLS as usize);
291    for offset in 0..leading {
292        let day: u32 = previous_days - leading + 1 + offset;
293        cells.push(EuvCalendarDay {
294            day,
295            muted: true,
296            today: (previous_year, previous_month, day) == today,
297        });
298    }
299    for day in 1..=month_days {
300        cells.push(EuvCalendarDay {
301            day,
302            muted: false,
303            today: (year, month, day) == today,
304        });
305    }
306    for day in 1..=trailing {
307        cells.push(EuvCalendarDay {
308            day,
309            muted: true,
310            today: (next_year, next_month, day) == today,
311        });
312    }
313    cells
314}
315
316/// Resolves a clicked grid cell into the date it stands for.
317///
318/// A cell of the month itself resolves to that day of the month. A muted
319/// cell belongs to the neighbouring month: the cells in front of the month
320/// carry the trailing days of the previous month, the cells behind it the
321/// leading days of the next one.
322///
323/// # Arguments
324///
325/// - `i64` - The year of the month on display.
326/// - `u32` - The month on display, `1` through `12`.
327/// - `bool` - Whether the clicked cell is one of the muted spill-over cells.
328///
329/// # Returns
330///
331/// - `Option<(i64, u32, u32)>` - The date the cell stands for, or `None`
332///   when the cell does not name a real date.
333pub fn resolve_clicked_date(
334    year: i64,
335    month: u32,
336    day: u32,
337    muted: bool,
338) -> Option<(i64, u32, u32)> {
339    if day == 0 {
340        return None;
341    }
342    let month_days: u32 = days_in_month(year, month);
343    if !muted {
344        if day > month_days {
345            return None;
346        }
347        return Some((year, month, day));
348    }
349    let leading: u32 = weekday_index(year, month, 1);
350    let trailing: u32 = GRID_CELLS - leading - month_days;
351    if day <= trailing {
352        let (next_year, next_month) = adjacent_month(year, month, 1);
353        return Some((next_year, next_month, day));
354    }
355    let (previous_year, previous_month) = adjacent_month(year, month, -1);
356    if day > days_in_month(previous_year, previous_month) {
357        return None;
358    }
359    Some((previous_year, previous_month, day))
360}
361
362/// Returns the current date of the host.
363///
364/// The browser's clock is read on the web and the system clock on the host,
365/// so the same code answers with the same calendar day in both worlds.
366///
367/// # Returns
368///
369/// - `(i64, u32, u32)` - The year, the month and the day of the month.
370fn current_date() -> (i64, u32, u32) {
371    #[cfg(target_arch = "wasm32")]
372    {
373        let now: js_sys::Date = js_sys::Date::new_0();
374        (
375            now.get_full_year() as i64,
376            now.get_month() + 1,
377            now.get_date(),
378        )
379    }
380    #[cfg(not(target_arch = "wasm32"))]
381    {
382        let seconds: i64 = SystemTime::now()
383            .duration_since(UNIX_EPOCH)
384            .map(|elapsed: Duration| elapsed.as_secs() as i64)
385            .unwrap_or_default();
386        civil_from_days(seconds.div_euclid(SECONDS_PER_DAY))
387    }
388}
389
390/// Returns the column headings of the day grid, Monday first.
391///
392/// # Returns
393///
394/// - `Vec<EuvCalendarWeekday>` - The headings, in display order.
395fn calendar_weekdays() -> Vec<EuvCalendarWeekday> {
396    vec![
397        EuvCalendarWeekday::Mon,
398        EuvCalendarWeekday::Tue,
399        EuvCalendarWeekday::Wed,
400        EuvCalendarWeekday::Thu,
401        EuvCalendarWeekday::Fri,
402        EuvCalendarWeekday::Sat,
403        EuvCalendarWeekday::Sun,
404    ]
405}
406
407/// Builds the click handler of the trigger.
408///
409/// The handler toggles the panel and, while opening it, marks the selected
410/// day so the grid shows where the current value sits.
411///
412/// # Arguments
413///
414/// - `Signal<bool>` - The panel's open state.
415/// - `Signal<u32>` - The day marked as selected in the grid.
416/// - `Signal<String>` - The signal holding the selected date.
417/// - `Signal<i64>` - The month on display.
418///
419/// # Returns
420///
421/// - `Option<Rc<dyn Fn(Event)>>` - The click handler of the trigger.
422fn on_trigger_click(
423    open: Signal<bool>,
424    selected: Signal<u32>,
425    value: Signal<String>,
426    month: Signal<i64>,
427) -> Option<Rc<dyn Fn(Event)>> {
428    Some(Rc::new(move |_event: Event| {
429        if open.get() {
430            open.set(false);
431            return;
432        }
433        open.set(true);
434        let day: u32 = match parse_date(&value.get()) {
435            Some((year, month_number, day)) if encode_month(year, month_number) == month.get() => {
436                day
437            }
438            _ => 0,
439        };
440        selected.set(day);
441    }))
442}
443
444/// Builds the click handler of the month chevrons.
445///
446/// The handler walks the month on display by the given number of months and
447/// clears the selected day, because the day number of the previous month
448/// means nothing in the new one.
449///
450/// # Arguments
451///
452/// - `Signal<i64>` - The month on display.
453/// - `Signal<u32>` - The day marked as selected in the grid.
454/// - `i64` - The number of months to walk.
455///
456/// # Returns
457///
458/// - `Option<Rc<dyn Fn(Event)>>` - The click handler of the chevron.
459fn on_shift_month(
460    month: Signal<i64>,
461    selected: Signal<u32>,
462    delta: i64,
463) -> Option<Rc<dyn Fn(Event)>> {
464    Some(Rc::new(move |_event: Event| {
465        month.set(month.get() + delta);
466        selected.set(0);
467    }))
468}
469
470/// Builds the callback a click on a day cell runs.
471///
472/// The callback receives the day number of the clicked cell and whether the
473/// cell belongs to a neighbouring month, resolves the cell into a date and
474/// publishes it. The click is handled by the cell itself rather than by a
475/// handler on the panel: the delegated dispatch runs the handler of the
476/// nearest marked ancestor, so a handler on the panel would never be reached
477/// from a cell that carries a handler of its own.
478///
479/// A date outside the bounds is refused: the panel clears the selection and
480/// leaves the value alone.
481///
482/// # Arguments
483///
484/// - `Signal<i64>` - The month on display.
485/// - `Signal<u32>` - The day marked as selected in the grid.
486/// - `Signal<String>` - The signal holding the selected date.
487/// - `Signal<bool>` - The panel's open state.
488/// - `Option<Rc<dyn Fn(&'static str)>>` - The callback receiving the date.
489/// - `&'static str` - The earliest selectable date.
490///
491/// # Returns
492///
493/// - `Rc<dyn Fn(u32, bool)>` - The callback of the day cells.
494fn on_day_click(
495    month: Signal<i64>,
496    selected: Signal<u32>,
497    value: Signal<String>,
498    open: Signal<bool>,
499    on_change: Option<Rc<dyn Fn(&'static str)>>,
500    min: &'static str,
501    max: &'static str,
502) -> Rc<dyn Fn(u32, bool)> {
503    Rc::new(move |day: u32, muted: bool| {
504        let (year, month_number) = decode_month(month.get());
505        let Some(picked) = resolve_clicked_date(year, month_number, day, muted) else {
506            return;
507        };
508        if !date_within_bounds(picked, min, max) {
509            selected.set(0);
510            return;
511        }
512        let formatted: String = format_date(picked.0, picked.1, picked.2);
513        let leaked: &'static str = Box::leak(formatted.clone().into_boxed_str());
514        value.set(formatted);
515        // The picked cell may belong to the neighbouring month, so the panel
516        // walks to the month of the pick. The selected day is the day within
517        // the month on display — keeping the number of the cell that was
518        // clicked would mark a different day of the month walked to.
519        month.set(encode_month(picked.0, picked.1));
520        selected.set(picked.2);
521        open.set(false);
522        if let Some(callback) = &on_change {
523            callback(leaked);
524        }
525    })
526}
527
528/// Builds the click handler of the "Today" button.
529///
530/// The handler selects today's date, walks the panel to the month of today
531/// and closes the panel. A date outside the bounds is refused.
532///
533/// # Arguments
534///
535/// - `Signal<String>` - The signal holding the selected date.
536/// - `Signal<bool>` - The panel's open state.
537/// - `Signal<i64>` - The month on display.
538/// - `Signal<u32>` - The day marked as selected in the grid.
539/// - `Option<Rc<dyn Fn(&'static str)>>` - The callback receiving the date.
540/// - `&'static str` - The earliest selectable date.
541///
542/// # Returns
543///
544/// - `Option<Rc<dyn Fn(Event)>>` - The click handler of the button.
545fn on_today(
546    value: Signal<String>,
547    open: Signal<bool>,
548    month: Signal<i64>,
549    selected: Signal<u32>,
550    on_change: Option<Rc<dyn Fn(&'static str)>>,
551    min: &'static str,
552    max: &'static str,
553) -> Option<Rc<dyn Fn(Event)>> {
554    Some(Rc::new(move |_event: Event| {
555        let (year, month_number, day) = current_date();
556        if !date_within_bounds((year, month_number, day), min, max) {
557            return;
558        }
559        let formatted: String = format_date(year, month_number, day);
560        let leaked: &'static str = Box::leak(formatted.clone().into_boxed_str());
561        value.set(formatted);
562        month.set(encode_month(year, month_number));
563        selected.set(day);
564        open.set(false);
565        if let Some(callback) = &on_change {
566            callback(leaked);
567        }
568    }))
569}
570
571/// Builds the click handler of the "Clear" button.
572///
573/// The handler empties the field and closes the panel.
574///
575/// # Arguments
576///
577/// - `Signal<String>` - The signal holding the selected date.
578/// - `Signal<bool>` - The panel's open state.
579/// - `Signal<u32>` - The day marked as selected in the grid.
580/// - `Option<Rc<dyn Fn(&'static str)>>` - The callback receiving the date.
581///
582/// # Returns
583///
584/// - `Option<Rc<dyn Fn(Event)>>` - The click handler of the button.
585fn on_clear(
586    value: Signal<String>,
587    open: Signal<bool>,
588    selected: Signal<u32>,
589    on_change: Option<Rc<dyn Fn(&'static str)>>,
590) -> Option<Rc<dyn Fn(Event)>> {
591    Some(Rc::new(move |_event: Event| {
592        value.set(String::new());
593        selected.set(0);
594        open.set(false);
595        if let Some(callback) = &on_change {
596            callback("");
597        }
598    }))
599}
600
601/// A date field backed by a calendar panel.
602///
603/// The component renders a `c_euv_date_picker` wrapper holding a trigger
604/// button and the panel. The trigger shows the selected date, or the
605/// placeholder while nothing is selected, and opens the panel; the panel
606/// carries the month on display, the [`euv_calendar`] grid and a footer with
607/// a "Today" and a "Clear" button.
608///
609/// The panel is always mounted: opening and closing it is a reactive class
610/// swap between `c_euv_date_picker_panel_open` and
611/// `c_euv_date_picker_panel_closed`, so the calendar never has to be
612/// rebuilt while the user types in the field.
613///
614/// The month on display and the open state are owned by the component, while
615/// the selected date lives in a caller-owned [`Signal`] formatted as
616/// `YYYY-MM-DD`, so the picker takes part in two-way binding. Days outside
617/// `min` and `max` cannot be picked.
618///
619/// The panel draws its own month caption and chevrons, because the calendar
620/// it reuses is pure presentation and leaves paging to its caller; the
621/// calendar keeps the inert chevrons of its own header as well.
622///
623/// # Arguments
624///
625/// - `VirtualNode<EuvDatePickerProps>` - The props node carrying the
626///   selected date, the bounds and the change callback.
627///
628/// # Returns
629///
630/// - `VirtualNode` - The date field with its calendar panel.
631#[component]
632pub fn euv_date_picker(node: VirtualNode<EuvDatePickerProps>) -> VirtualNode {
633    let EuvDatePickerProps {
634        value,
635        placeholder,
636        label: label_text,
637        min,
638        max,
639        on_change,
640    }: EuvDatePickerProps = node.try_get_props().unwrap_or_default();
641    let selected_value: String = value.get();
642    let today: (i64, u32, u32) = current_date();
643    let parsed: Option<(i64, u32, u32)> = parse_date(&selected_value);
644    // An empty field has no selected day: the grid must not mark one. The
645    // month on display still starts at the current month, so the panel opens
646    // on the month the user is looking at.
647    let initial_month: i64 = match parsed {
648        Some((year, month_number, _)) => encode_month(year, month_number),
649        None => encode_month(today.0, today.1),
650    };
651    let initial_selected: u32 = parsed.map(|(_, _, day): (i64, u32, u32)| day).unwrap_or(0);
652    let is_empty: bool = selected_value.is_empty();
653    let open: Signal<bool> = HookContext::use_hook(move || Signal::create(false));
654    let month: Signal<i64> = HookContext::use_hook(move || Signal::create(initial_month));
655    let selected: Signal<u32> = HookContext::use_hook(move || Signal::create(initial_selected));
656    let caption_cache: MonthCaption = HookContext::use_hook(MonthCaption::default);
657    let (year, month_number) = decode_month(month.get());
658    let caption: &'static str = caption_cache.caption(year, month_number);
659    let days: Vec<EuvCalendarDay> = month_grid(year, month_number, today);
660    let day_click: Rc<dyn Fn(u32, bool)> =
661        on_day_click(month, selected, value, open, on_change.clone(), min, max);
662    let calendar_node: VirtualNode<EuvCalendarProps> = VirtualNode::Element {
663        tag: Tag::Element(Cow::Borrowed("div")),
664        attributes: Vec::new(),
665        children: Vec::new(),
666        key: None,
667        props: Some(Box::new(EuvCalendarProps {
668            title: "",
669            weekdays: calendar_weekdays(),
670            days,
671            selected,
672            on_day_click: Some(day_click),
673        })),
674    };
675    let calendar: VirtualNode = euv_calendar(calendar_node);
676    html! {
677        div {
678            class: c_euv_date_picker()
679            if !label_text.is_empty() {
680                label {
681                    class: c_form_label()
682                    label_text
683                }
684            }
685            button {
686                type: "button"
687                class: c_euv_date_picker_trigger()
688                onclick: on_trigger_click(open, selected, value, month)
689                span {
690                    class: if is_empty {
691                        c_euv_date_picker_placeholder()
692                    } else {
693                        c_euv_date_picker_value()
694                    }
695                    // The text has to be a child of the element, not a
696                    // braced dynamic node: a braced node would capture the
697                    // value it was rendered with and never learn about the
698                    // pick that follows.
699                    if is_empty {
700                        placeholder
701                    } else {
702                        selected_value
703                    }
704                }
705                span {
706                    class: c_euv_date_picker_caret()
707                    CARET_GLYPH
708                }
709            }
710            div {
711                class: c_euv_date_picker_panel()
712                class: if { open } {
713                    c_euv_date_picker_panel_open()
714                } else {
715                    c_euv_date_picker_panel_closed()
716                }
717                div {
718                    class: c_euv_calendar_nav()
719                    button {
720                        type: "button"
721                        class: c_euv_date_picker_nav()
722                        onclick: on_shift_month(month, selected, -1)
723                        PREVIOUS_MONTH_GLYPH
724                    }
725                    span {
726                        class: c_euv_calendar_title()
727                        caption
728                    }
729                    button {
730                        type: "button"
731                        class: c_euv_date_picker_nav()
732                        onclick: on_shift_month(month, selected, 1)
733                        NEXT_MONTH_GLYPH
734                    }
735                }
736                calendar
737                div {
738                    class: c_euv_date_picker_footer()
739                    euv_button {
740                        variant: EuvButtonVariant::Outline
741                        label: TODAY_LABEL
742                        onclick: on_today(value, open, month, selected, on_change.clone(), min, max)
743                    }
744                    euv_button {
745                        variant: EuvButtonVariant::Outline
746                        label: CLEAR_LABEL
747                        onclick: on_clear(value, open, selected, on_change.clone())
748                    }
749                }
750            }
751        }
752    }
753}