Date Picker
Rust/UI component that displays a date picker.
- Copy Demo
Mo | Tu | We | Th | Fr | Sa | Su |
---|---|---|---|---|---|---|
28 | 29 | 30 | 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 1 |
use icons::{ChevronLeft, ChevronRight};
use leptos::prelude::*;
use time::{Date, Month};
use crate::components::_coming_soon::date_picker::{
DatePicker, DatePickerCell, DatePickerHeader, DatePickerNavButton, DatePickerRow, DatePickerTitle,
DatePickerWeekDay,
};
use crate::components::_coming_soon::date_picker_state::{DatePickerDay, DatePickerState};
use crate::utilities::query::QueryUtils;
#[component]
pub fn DemoDatePicker() -> impl IntoView {
// Initialize dates from URL or use defaults
let default_start = Date::from_calendar_date(2025, Month::May, 5).expect("Invalid date");
let default_end = Date::from_calendar_date(2025, Month::May, 14).expect("Invalid date");
let (picker_state, setup_url_sync) = DatePickerState::from_url_or_default(default_start, default_end);
// Set up URL synchronization
setup_url_sync();
// Extract start and end dates as RwSignal
let start_date = RwSignal::new(picker_state.get().start_date);
let end_date = RwSignal::new(picker_state.get().end_date);
// Track which month is currently displayed (independent from selected dates)
let display_date = RwSignal::new(picker_state.get().start_date);
// Sync local signals with picker_state when URL changes
Effect::new(move |_| {
let state = picker_state.get();
start_date.set(state.start_date);
end_date.set(state.end_date);
// Don't reset display_date here - let user navigation control it
});
// Navigation: Go to previous month
let go_to_previous_month = move |_| {
let current = display_date.get();
let new_date = if current.month() == Month::January {
Date::from_calendar_date(current.year() - 1, Month::December, 1)
} else {
Date::from_calendar_date(current.year(), current.month().previous(), 1)
}
.expect("Invalid date");
display_date.set(new_date);
};
// Navigation: Go to next month
let go_to_next_month = move |_| {
let current = display_date.get();
let new_date = if current.month() == Month::December {
Date::from_calendar_date(current.year() + 1, Month::January, 1)
} else {
Date::from_calendar_date(current.year(), current.month().next(), 1)
}
.expect("Invalid date");
display_date.set(new_date);
};
// Handle day click
let handle_day_click = move |day: u8| {
if day == 0 {
return;
}
let year = display_date.get().year();
let month = display_date.get().month();
let new_date = Date::from_calendar_date(year, month, day).expect("Invalid date");
// Determine which date to update based on proximity (using full dates)
let current_start = start_date.get();
let current_end = end_date.get();
let days_from_start = (new_date - current_start).whole_days().abs();
let days_from_end = (new_date - current_end).whole_days().abs();
if days_from_start <= days_from_end {
start_date.set(new_date);
} else {
end_date.set(new_date);
}
// Ensure start_date <= end_date by swapping if needed
if start_date.get() > end_date.get() {
let temp = start_date.get();
start_date.set(end_date.get());
end_date.set(temp);
}
QueryUtils::update_dates_url(Some(start_date.get()), Some(end_date.get()));
};
view! {
<DatePicker>
<DatePickerHeader>
<DatePickerTitle attr:role="presentation">
{move || display_date.get().month().to_string()} {move || display_date.get().year()}
</DatePickerTitle>
<div class="flex items-center space-x-1">
<DatePickerNavButton
attr:title="previous-month"
attr:aria-label="Go to previous month"
class="left-1"
on:click=go_to_previous_month
>
<ChevronLeft />
</DatePickerNavButton>
<DatePickerNavButton
attr:title="next-month"
attr:aria-label="Go to next month"
class="right-1"
on:click=go_to_next_month
>
<ChevronRight />
</DatePickerNavButton>
</div>
</DatePickerHeader>
<table class="space-y-1 w-full border-collapse" role="grid">
<thead>
<tr class="flex">
<DatePickerWeekDay attr:aria-label="Monday">Mo</DatePickerWeekDay>
<DatePickerWeekDay attr:aria-label="Tuesday">Tu</DatePickerWeekDay>
<DatePickerWeekDay attr:aria-label="Wednesday">We</DatePickerWeekDay>
<DatePickerWeekDay attr:aria-label="Thursday">Th</DatePickerWeekDay>
<DatePickerWeekDay attr:aria-label="Friday">Fr</DatePickerWeekDay>
<DatePickerWeekDay attr:aria-label="Saturday">Sa</DatePickerWeekDay>
<DatePickerWeekDay attr:aria-label="Sunday">Su</DatePickerWeekDay>
</tr>
</thead>
<tbody>
{move || {
let year = display_date.get().year();
let month = display_date.get().month();
let days = DatePickerState::get_calendar_days(year, month);
let weeks: Vec<Vec<DatePickerDay>> = days.chunks(7).map(|chunk| chunk.to_vec()).collect();
view! {
<For
each=move || weeks.clone()
key=|week| week.first().map(|d| d.day).unwrap_or(0)
children=move |week| {
let year = year;
let month = month;
view! {
<DatePickerRow>
<For
each=move || week.clone()
key=|DatePickerDay { day, disabled }| format!("{day}-{disabled}")
children=move |DatePickerDay { day, disabled }| {
view! {
<DatePickerCell
day=day
year=year
month=month
disabled=disabled
start_date=start_date
end_date=end_date
on_click=handle_day_click
/>
}
}
/>
</DatePickerRow>
}
}
/>
}
}}
</tbody>
</table>
</DatePicker>
}
}
Installation
# Coming soon :)
Usage
// Coming soon 🦀
Examples
Date Picker Dual
- Copy Demo
Mo | Tu | We | Th | Fr | Sa | Su |
---|---|---|---|---|---|---|
28 | 29 | 30 | 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 1 |
Mo | Tu | We | Th | Fr | Sa | Su |
---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 31 | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 1 | 2 | 3 | 4 | 5 | 6 |
use icons::{ChevronLeft, ChevronRight};
use leptos::prelude::*;
use time::{Date, Month};
use crate::components::_coming_soon::date_picker::{
DatePicker, DatePickerCell, DatePickerHeader, DatePickerNavButton, DatePickerRow, DatePickerTitle,
DatePickerWeekDay,
};
use crate::components::_coming_soon::date_picker_state::{DatePickerDay, DatePickerState};
use crate::utilities::query::QueryUtils;
#[component]
pub fn DemoDatePickerDual() -> impl IntoView {
// Initialize dates from URL or use defaults
let default_start = Date::from_calendar_date(2025, Month::May, 5).expect("Invalid date");
let default_end = Date::from_calendar_date(2025, Month::June, 8).expect("Invalid date");
let (picker_state, setup_url_sync) = DatePickerState::from_url_or_default(default_start, default_end);
// Set up URL synchronization
setup_url_sync();
// Extract start and end dates as RwSignal
let start_date = RwSignal::new(picker_state.get().start_date);
let end_date = RwSignal::new(picker_state.get().end_date);
// Track the left calendar month (right will be left + 1)
let left_display_date = RwSignal::new(picker_state.get().start_date);
// Sync local signals with picker_state when URL changes
Effect::new(move |_| {
let state = picker_state.get();
start_date.set(state.start_date);
end_date.set(state.end_date);
});
// Navigation: Go to previous 2 months
let go_to_previous = move |_| {
let current = left_display_date.get();
let new_date = if current.month() == Month::January {
Date::from_calendar_date(current.year() - 1, Month::November, 1)
} else if current.month() == Month::February {
Date::from_calendar_date(current.year() - 1, Month::December, 1)
} else {
Date::from_calendar_date(current.year(), current.month().previous().previous(), 1)
}
.expect("Invalid date");
left_display_date.set(new_date);
};
// Navigation: Go to next 2 months
let go_to_next = move |_| {
let current = left_display_date.get();
let new_date = if current.month() == Month::November {
Date::from_calendar_date(current.year() + 1, Month::January, 1)
} else if current.month() == Month::December {
Date::from_calendar_date(current.year() + 1, Month::February, 1)
} else {
Date::from_calendar_date(current.year(), current.month().next().next(), 1)
}
.expect("Invalid date");
left_display_date.set(new_date);
};
// Handle day click - works for both calendars
let handle_day_click = move |year: i32, month: Month, day: u8| {
if day == 0 {
return;
}
let new_date = Date::from_calendar_date(year, month, day).expect("Invalid date");
// Determine which date to update based on proximity (using full dates)
let current_start = start_date.get();
let current_end = end_date.get();
let days_from_start = (new_date - current_start).whole_days().abs();
let days_from_end = (new_date - current_end).whole_days().abs();
if days_from_start <= days_from_end {
start_date.set(new_date);
} else {
end_date.set(new_date);
}
// Ensure start_date <= end_date by swapping if needed
if start_date.get() > end_date.get() {
let temp = start_date.get();
start_date.set(end_date.get());
end_date.set(temp);
}
QueryUtils::update_dates_url(Some(start_date.get()), Some(end_date.get()));
};
// Helper to render a single calendar
let render_calendar = move |is_left: bool| {
let display_date = if is_left {
left_display_date.get()
} else {
// Right calendar is left + 1 month
let left = left_display_date.get();
if left.month() == Month::December {
Date::from_calendar_date(left.year() + 1, Month::January, 1)
} else {
Date::from_calendar_date(left.year(), left.month().next(), 1)
}
.expect("Invalid date")
};
let year = display_date.year();
let month = display_date.month();
view! {
<DatePicker>
<DatePickerHeader>
<DatePickerTitle attr:role="presentation">{month.to_string()} {year}</DatePickerTitle>
{move || {
if is_left {
view! {
<div class="flex items-center space-x-1">
<DatePickerNavButton
attr:title="previous-months"
attr:aria-label="Go to previous months"
attr:disabled=true
class="left-1"
on:click=go_to_previous
>
<ChevronLeft />
</DatePickerNavButton>
</div>
}
.into_any()
} else {
view! {
<div class="flex items-center space-x-1">
<DatePickerNavButton
attr:title="next-months"
attr:aria-label="Go to next months"
class="right-1"
attr:disabled=true
on:click=go_to_next
>
<ChevronRight />
</DatePickerNavButton>
</div>
}
.into_any()
}
}}
</DatePickerHeader>
<table class="space-y-1 w-full border-collapse" role="grid">
<thead>
<tr class="flex">
<DatePickerWeekDay attr:aria-label="Monday">Mo</DatePickerWeekDay>
<DatePickerWeekDay attr:aria-label="Tuesday">Tu</DatePickerWeekDay>
<DatePickerWeekDay attr:aria-label="Wednesday">We</DatePickerWeekDay>
<DatePickerWeekDay attr:aria-label="Thursday">Th</DatePickerWeekDay>
<DatePickerWeekDay attr:aria-label="Friday">Fr</DatePickerWeekDay>
<DatePickerWeekDay attr:aria-label="Saturday">Sa</DatePickerWeekDay>
<DatePickerWeekDay attr:aria-label="Sunday">Su</DatePickerWeekDay>
</tr>
</thead>
<tbody>
{move || {
let days = DatePickerState::get_calendar_days(year, month);
let weeks: Vec<Vec<DatePickerDay>> = days.chunks(7).map(|chunk| chunk.to_vec()).collect();
view! {
<For
each=move || weeks.clone()
key=|week| week.first().map(|d| d.day).unwrap_or(0)
children=move |week| {
view! {
<DatePickerRow>
<For
each=move || week.clone()
key=|DatePickerDay { day, disabled }| format!("{day}-{disabled}")
children=move |DatePickerDay { day, disabled }| {
view! {
<DatePickerCell
day=day
year=year
month=month
disabled=disabled
start_date=start_date
end_date=end_date
on_click=move |d| handle_day_click(year, month, d)
/>
}
}
/>
</DatePickerRow>
}
}
/>
}
}}
</tbody>
</table>
</DatePicker>
}
};
view! { <div class="flex gap-4">{render_calendar(true)} {render_calendar(false)}</div> }
}