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}