Date Picker

Rust/UI component that displays a date picker.

  • Rust/UI Icons - CopyCopy Demo
May2025
MoTuWeThFrSaSu
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

  • Rust/UI Icons - CopyCopy Demo
May2025
MoTuWeThFrSaSu
June2025
MoTuWeThFrSaSu
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> }
}