use leptos::callback::Callback;
use leptos::prelude::Callable;
use leptos::prelude::OnAttribute;
use leptos::prelude::event_target_value;
use leptos::prelude::{
ClassAttribute, CustomAttribute, ElementChild, Get, GetUntracked, GlobalAttributes, IntoAny,
IntoView, Signal, StyleAttribute, component, view,
};
use crate::elements::icon::Icon;
use crate::util::{Size, TestAttr};
#[allow(unused_imports)]
use leptos::prelude::Effect;
fn size_class(size: Size) -> &'static str {
match size {
Size::Small => "is-small",
Size::Normal => "is-normal",
Size::Medium => "is-medium",
Size::Large => "is-large",
}
}
#[component]
pub fn TextArea(
#[prop(into)]
name: Signal<String>,
#[prop(into)]
value: Signal<String>,
update: Callback<String>,
#[prop(optional, into)]
classes: Signal<String>,
#[prop(optional, into)]
placeholder: Signal<String>,
#[prop(optional)]
rows: Option<u32>,
#[prop(optional)]
size: Option<Size>,
#[prop(optional, into)]
fixed_size: Signal<bool>,
#[prop(optional, into)]
loading: Signal<bool>,
#[prop(optional, into)]
disabled: Signal<bool>,
#[prop(optional, into)]
readonly: Signal<bool>,
#[prop(optional, into)]
r#static: Signal<bool>,
#[prop(optional, into)]
is_genai: Signal<bool>,
#[prop(optional, into)]
test_attr: Option<TestAttr>,
) -> impl IntoView {
let class = {
let classes = classes.clone();
let loading = loading.clone();
let fixed_size = fixed_size.clone();
let r#static = r#static.clone();
move || {
let mut parts = vec!["textarea".to_string()];
let extra = classes.get();
if !extra.trim().is_empty() {
parts.push(extra);
}
if let Some(sz) = size {
parts.push(size_class(sz).to_string());
}
if loading.get() {
parts.push("is-loading".to_string());
}
if r#static.get() {
parts.push("is-static".to_string());
}
if fixed_size.get() {
parts.push("has-fixed-size".to_string());
}
parts.join(" ")
}
};
let (data_testid_opt, data_cy_opt) = 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),
};
let name_value = name.get_untracked();
let placeholder_value = placeholder.get_untracked();
let is_disabled = disabled.get_untracked();
let is_readonly = readonly.get_untracked();
let rows_value = rows.unwrap_or(0).to_string();
let initial_value = value.get_untracked();
move || {
let data_testid = data_testid_opt.clone();
let data_cy = data_cy_opt.clone();
if is_genai.get() {
view! {
<div
id="context"
style="position:relative"
attr:data-testid=move || data_testid.clone()
attr:data-cy=move || data_cy.clone()
>
<Icon size=Size::Small classes="is-pulled-right ribbon">
<i class="fa-brands fa-openai"></i>
</Icon>
<textarea
name=name_value.clone()
class=move || class()
placeholder=placeholder_value.clone()
disabled=is_disabled
readonly=is_readonly
rows=rows_value.clone()
on:input=move |ev| {
update.run(event_target_value(&ev));
}
>
{initial_value.clone()}
</textarea>
</div>
}
.into_any()
} else {
view! {
<textarea
name=name_value.clone()
class=move || class()
placeholder=placeholder_value.clone()
disabled=is_disabled
readonly=is_readonly
rows=rows_value.clone()
attr:data-testid=move || data_testid.clone()
attr:data-cy=move || data_cy.clone()
on:input=move |ev| {
update.run(event_target_value(&ev));
}
>
{initial_value.clone()}
</textarea>
}
.into_any()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::Size;
use leptos::prelude::RenderHtml;
fn noop() -> Callback<String> {
Callback::new(|_v: String| {})
}
#[test]
fn textarea_renders_default_class() {
let html = view! { <TextArea name="notes" value="" update=noop() /> }.to_html();
assert!(
html.contains(r#"class="textarea""#),
"expected base 'textarea' class; got: {}",
html
);
assert!(
html.contains(r#"name="notes""#),
"expected name attribute; got: {}",
html
);
}
#[test]
fn textarea_loading_size_and_fixed() {
let html = view! {
<TextArea
name="n"
value="v"
size=Size::Small
loading=true
fixed_size=true
update=noop()
/>
}
.to_html();
assert!(
html.contains("is-loading"),
"expected is-loading; got: {}",
html
);
assert!(
html.contains("is-small"),
"expected is-small; got: {}",
html
);
assert!(
html.contains("has-fixed-size"),
"expected has-fixed-size; got: {}",
html
);
}
#[test]
fn textarea_rows_and_placeholder_and_flags() {
let html = view! {
<TextArea
name="n"
value="v"
rows=6
placeholder="type here"
disabled=true
readonly=true
update=noop()
/>
}
.to_html();
assert!(
html.contains(r#"rows="6""#),
"expected rows attr; got: {}",
html
);
assert!(
html.contains(r#"placeholder="type here""#),
"expected placeholder; got: {}",
html
);
assert!(
html.contains("disabled"),
"expected disabled; got: {}",
html
);
assert!(
html.contains("readonly"),
"expected readonly; got: {}",
html
);
}
#[test]
fn textarea_genai_ribbon() {
let html = view! { <TextArea name="g" value="" is_genai=true update=noop() /> }.to_html();
assert!(
html.contains("ribbon"),
"expected ribbon icon when is_genai; got: {}",
html
);
}
}
#[cfg(all(test, target_arch = "wasm32"))]
mod wasm_tests {
use super::*;
use crate::util::{Size, TestAttr};
use leptos::prelude::*;
use wasm_bindgen_test::*;
fn noop() -> Callback<String> {
Callback::new(|_v: String| {})
}
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn textarea_renders_test_attr_as_data_testid() {
let html = view! {
<TextArea name="notes" value="" update=noop() test_attr=TestAttr::test_id("textarea-test") />
}
.to_html();
assert!(
html.contains(r#"data-testid="textarea-test""#),
"expected data-testid attribute; got: {}",
html
);
}
#[wasm_bindgen_test]
fn textarea_no_test_attr_when_not_provided() {
let html = view! { <TextArea name="notes" value="" update=noop() /> }.to_html();
assert!(
!html.contains("data-testid") && !html.contains("data-cy"),
"expected no data attribute; got: {}",
html
);
}
}