orbital-date-pickers 0.1.1

Date and time picker components for Orbital
Documentation
use leptos::prelude::*;
use orbital_core_components::{Button, ButtonAppearance, ButtonGroup, ButtonShape, ButtonSize};

use crate::shared::{
    hour_markers, is_minute_step_disabled, marker_position_style, minute_display_markers,
    snap_minute, tick_endpoints, to_twelve_hour, to_twenty_four_hour, DEFAULT_FACE_SIZE,
};

use super::clock_number::ClockNumber;
use super::interaction::ClockInteractionOverlay;
use super::pointer::ClockPointer;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ClockView {
    Hours,
    Minutes,
}

#[component]
pub fn TimeClockDial(
    view: RwSignal<ClockView>,
    draft_hour_24: RwSignal<u32>,
    draft_minute: RwSignal<u32>,
    is_pm: RwSignal<bool>,
    ampm: Signal<bool>,
    minute_step: Signal<u32>,
    disabled: Signal<bool>,
    on_minute_selected: Callback<(u32, u32)>,
) -> impl IntoView {
    view! {
        <div class="orb-picker-time-clock__face">
            <svg class="orb-picker-time-clock__svg" viewBox="0 0 260 260" aria-hidden="true">
                <circle class="orb-picker-time-clock__circle" cx="130" cy="130" r="125" />
                {(0..12).map(|index| {
                    let (x1, y1, x2, y2) = tick_endpoints(index, DEFAULT_FACE_SIZE);
                    view! {
                        <line
                            class="orb-picker-time-clock__tick"
                            x1=x1
                            y1=y1
                            x2=x2
                            y2=y2
                        />
                    }
                }).collect_view()}
                <circle class="orb-picker-time-clock__center" cx="130" cy="130" r="4" />
            </svg>

            <ClockPointer
                view=Signal::derive(move || view.get())
                draft_hour_24=Signal::derive(move || draft_hour_24.get())
                draft_minute=Signal::derive(move || draft_minute.get())
                ampm=ampm
            />

            {move || {
                match view.get() {
                    ClockView::Hours => hour_numbers_view(
                        ampm,
                        disabled,
                        draft_hour_24,
                        is_pm,
                        view,
                    )
                    .into_any(),
                    ClockView::Minutes => minute_numbers_view(
                        minute_step,
                        disabled,
                        draft_hour_24,
                        draft_minute,
                        view,
                        on_minute_selected,
                    )
                    .into_any(),
                }
            }}

            <ClockInteractionOverlay
                view=view
                draft_hour_24=draft_hour_24
                draft_minute=draft_minute
                is_pm=is_pm
                ampm=ampm
                minute_step=minute_step
                disabled=disabled
                on_minute_selected=on_minute_selected
            />
        </div>
        {move || {
            ampm.get().then(|| {
                view! {
                    <div class="orb-picker-time-clock__meridiem">
                        <ButtonGroup>
                            <Button
                                appearance=Signal::derive(move || {
                                    if is_pm.get() {
                                        ButtonAppearance::Secondary
                                    } else {
                                        ButtonAppearance::Primary
                                    }
                                })
                                shape=Signal::from(ButtonShape::Rounded)
                                size=Signal::from(ButtonSize::Small)
                                disabled=disabled
                                on_click=Callback::new(move |_| {
                                    if is_pm.get() {
                                        is_pm.set(false);
                                        let (display, _) = to_twelve_hour(draft_hour_24.get());
                                        draft_hour_24.set(to_twenty_four_hour(display, false));
                                    }
                                })
                            >
                                "AM"
                            </Button>
                            <Button
                                appearance=Signal::derive(move || {
                                    if is_pm.get() {
                                        ButtonAppearance::Primary
                                    } else {
                                        ButtonAppearance::Secondary
                                    }
                                })
                                shape=Signal::from(ButtonShape::Rounded)
                                size=Signal::from(ButtonSize::Small)
                                disabled=disabled
                                on_click=Callback::new(move |_| {
                                    if !is_pm.get() {
                                        is_pm.set(true);
                                        let (display, _) = to_twelve_hour(draft_hour_24.get());
                                        draft_hour_24.set(to_twenty_four_hour(display, true));
                                    }
                                })
                            >
                                "PM"
                            </Button>
                        </ButtonGroup>
                    </div>
                }
            })
        }}
    }
}

fn hour_numbers_view(
    ampm: Signal<bool>,
    disabled: Signal<bool>,
    draft_hour_24: RwSignal<u32>,
    is_pm: RwSignal<bool>,
    view: RwSignal<ClockView>,
) -> impl IntoView {
    let markers = hour_markers(ampm.get_untracked());
    let count = markers.len();
    markers
        .into_iter()
        .enumerate()
        .map(|(index, marker)| {
            let (left, top) = marker_position_style(index, count, false);
            let display = if ampm.get_untracked() {
                marker.to_string()
            } else {
                format!("{marker:02}")
            };
            let aria_label = format!("{display} hours");
            view! {
                <ClockNumber
                    label=display.clone()
                    selected=Signal::derive(move || {
                        let selected = draft_hour_24.get();
                        if ampm.get() {
                            to_twelve_hour(selected).0 == marker
                        } else {
                            selected == marker
                        }
                    })
                    disabled=disabled
                    left=left
                    top=top
                    aria_label=aria_label
                    on_select=Callback::new(move |_| {
                        let hour_24 = if ampm.get_untracked() {
                            to_twenty_four_hour(marker, is_pm.get_untracked())
                        } else {
                            marker
                        };
                        draft_hour_24.set(hour_24);
                        view.set(ClockView::Minutes);
                    })
                />
            }
        })
        .collect_view()
}

fn minute_numbers_view(
    minute_step: Signal<u32>,
    disabled: Signal<bool>,
    draft_hour_24: RwSignal<u32>,
    draft_minute: RwSignal<u32>,
    view: RwSignal<ClockView>,
    on_minute_selected: Callback<(u32, u32)>,
) -> impl IntoView {
    let markers = minute_display_markers();
    let count = markers.len();
    markers
        .into_iter()
        .enumerate()
        .map(|(index, minute)| {
            let (left, top) = marker_position_style(index, count, false);
            let label = format!("{minute:02}");
            let aria_label = format!("{label} minutes");
            view! {
                <ClockNumber
                    label=label.clone()
                    selected=Signal::derive(move || {
                        snap_minute(draft_minute.get(), 5) == minute
                    })
                    disabled=Signal::derive(move || {
                        disabled.get()
                            || is_minute_step_disabled(minute, minute_step.get())
                    })
                    left=left
                    top=top
                    aria_label=aria_label
                    on_select=Callback::new(move |_| {
                        draft_minute.set(minute);
                        on_minute_selected.run((draft_hour_24.get_untracked(), minute));
                        view.set(ClockView::Hours);
                    })
                />
            }
        })
        .collect_view()
}