hikari-components 0.2.2

Core UI components (40+) for the Hikari design system
// hi-components/src/basic/radio_group.rs
// RadioGroup component

use hikari_palette::classes::{ClassesBuilder, RadioClass};
use tairitsu_hooks::ReactiveSignal;

use crate::prelude::*;
use crate::styled::StyledComponent;

#[derive(Clone)]
pub struct RadioContext {
    pub name: String,
    pub selected_value: ReactiveSignal<String>,
    pub on_change: EventHandler<String>,
    pub disabled: bool,
}

#[define_props]
pub struct RadioButtonProps {
    pub value: String,
    pub children: Element,
    #[default(false)]
    pub disabled: bool,
    #[default(String::new())]
    pub class: String,
    pub on_change: Option<EventHandler<String>>,
}

#[component]
pub fn RadioButton(props: RadioButtonProps) -> Element {
    let ctx = use_context::<RadioContext>().expect("RadioContext not found");
    let ctx = ctx.get();
    let is_checked = *ctx.selected_value.read() == props.value;
    let disabled = props.disabled || ctx.disabled;

    let radio_name = ctx.name.to_string();
    let on_change = ctx.on_change.clone();

    let radio_classes = ClassesBuilder::new()
        .add_typed(RadioClass::Label)
        .add(&props.class)
        .build();

    let handle_change = {
        let value = props.value.clone();
        let on_change = props.on_change;
        let group_on_change = ctx.on_change.clone();
        let group_selected_value = ctx.selected_value.clone();
        move |_| {
            group_selected_value.set(value.clone());
            group_on_change.call(value.clone());
            if let Some(handler) = on_change.as_ref() {
                handler.call(value.clone());
            }
        }
    };

    rsx! {
        label { class: radio_classes,
            input {
                r#type: "radio",
                name: radio_name,
                value: "{props.value}",
                checked: is_checked,
                disabled,
                onchange: handle_change,
            }
            div { class: "hi-radio-indicator",
                div { class: "hi-radio-dot" }
            }
            span { class: "hi-radio-text", {props.children} }
        }
    }
}

#[define_props]
pub struct RadioGroupProps {
    pub name: String,
    pub value: String,
    pub on_change: Option<EventHandler<String>>,
    #[default(false)]
    pub disabled: bool,
    pub children: Element,
    #[default(String::new())]
    pub class: String,
    pub direction: RadioDirection,
}

#[derive(Clone, Copy, PartialEq, Debug, Default)]
pub enum RadioDirection {
    #[default]
    Vertical,
    Horizontal,
}

#[component]
pub fn RadioGroup(props: RadioGroupProps) -> Element {
    let selected_value = use_signal(|| props.value.clone());

    let name = props.name.clone();
    let disabled = props.disabled;
    let on_change = props
        .on_change
        .clone()
        .unwrap_or_else(|| EventHandler::new(|_| {}));

    let _ctx = use_context_provider(move || RadioContext {
        name,
        selected_value,
        on_change,
        disabled,
    });

    let direction_class = match props.direction {
        RadioDirection::Vertical => RadioClass::RadioGroupVertical,
        RadioDirection::Horizontal => RadioClass::RadioGroupHorizontal,
    };

    let group_classes = ClassesBuilder::new()
        .add_typed(RadioClass::RadioGroup)
        .add_typed(direction_class)
        .add(&props.class)
        .build();

    rsx! {
        div { class: group_classes, {props.children} }
    }
}

pub struct RadioGroupComponent;

impl StyledComponent for RadioGroupComponent {
    fn styles() -> &'static str {
        include_str!(concat!(env!("OUT_DIR"), "/styles/radio.css"))
    }

    fn name() -> &'static str {
        "radio_group"
    }
}