use crate::util::TestAttr;
use leptos::callback::Callable;
use leptos::prelude::{
Callback, Children, ClassAttribute, CustomAttribute, ElementChild, GetUntracked, IntoView,
OnAttribute, Signal, component, event_target_value, view,
};
#[component]
pub fn Radio(
#[prop(into)]
name: Signal<String>,
#[prop(into)]
value: Signal<String>,
#[prop(optional)]
checked_value: Option<String>,
update: Callback<String>,
children: Children,
#[prop(optional, into)]
classes: Signal<String>,
#[prop(optional, into)]
disabled: Signal<bool>,
#[prop(optional, into)]
test_attr: Option<TestAttr>,
) -> impl IntoView {
let name_value = name.get_untracked();
let value_value = value.get_untracked();
let class_value = {
let extra = classes.get_untracked().trim().to_string();
if extra.is_empty() {
"radio".to_string()
} else {
format!("radio {}", extra)
}
};
let is_disabled = disabled.get_untracked();
let is_checked = checked_value
.as_ref()
.is_some_and(|checked_value| checked_value == &value_value);
let (data_testid, data_cy) = match &test_attr {
Some(attr) if attr.key == "data-testid" => (Some(attr.value.clone()), None),
Some(attr) if attr.key == "data-cy" => (None, Some(attr.value.clone())),
_ => (None, None),
};
view! {
<label
class=class_value
attr:data-testid=data_testid
attr:data-cy=data_cy
>
<input
type="radio"
name=name_value
value=value_value
checked=is_checked
disabled=is_disabled
on:change=move |v| update.run(event_target_value(&v))
/>
{children()}
</label>
}
}
#[cfg(test)]
mod tests {
use super::*;
use leptos::prelude::RenderHtml;
fn noop() -> Callback<String> {
Callback::new(|_v| {})
}
#[test]
fn radio_renders_base_class() {
let html =
view! { <Radio name="group" value="A" update=noop()>"Option A"</Radio> }.to_html();
assert!(
html.contains(r#"class="radio""#),
"expected base 'radio' class; got: {}",
html
);
assert!(
html.contains("Option A"),
"expected children rendered; got: {}",
html
);
}
#[test]
fn radio_checked_matches_checked_value() {
let html = view! { <Radio name="g" value="A" checked_value="A".to_string() update=noop()>"A"</Radio> }.to_html();
assert!(
html.contains(r#"checked"#),
"expected 'checked' present when values match; got: {}",
html
);
let html_unchecked = view! { <Radio name="g" value="A" checked_value="B".to_string() update=noop()>"A"</Radio> }.to_html();
assert!(
!html_unchecked.contains(r#"checked"#),
"did not expect 'checked' when values differ; got: {}",
html_unchecked
);
}
#[test]
fn radio_respects_disabled() {
let html =
view! { <Radio name="g" value="A" disabled=true update=noop()>"A"</Radio> }.to_html();
assert!(
html.contains(r#"disabled"#),
"expected 'disabled' attribute; got: {}",
html
);
}
}
#[cfg(all(test, target_arch = "wasm32"))]
mod wasm_tests {
use super::*;
use crate::util::TestAttr;
use leptos::prelude::*;
use wasm_bindgen_test::*;
fn noop() -> Callback<String> {
Callback::new(|_v| {})
}
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn radio_renders_test_attr_as_data_testid() {
let html = view! {
<Radio name="group" value="A" update=noop() test_attr=TestAttr::test_id("radio-test")>"Option A"</Radio>
}
.to_html();
assert!(
html.contains(r#"data-testid="radio-test""#),
"expected data-testid attribute; got: {}",
html
);
}
#[wasm_bindgen_test]
fn radio_no_test_attr_when_not_provided() {
let html = view! {
<Radio name="group" value="A" update=noop()>"Option A"</Radio>
}
.to_html();
assert!(
!html.contains("data-testid") && !html.contains("data-cy"),
"expected no data attribute; got: {}",
html
);
}
}