ev_lib 0.4.0

EV-invest shared Rust libraries, one per feature
Documentation
use dioxus::prelude::*;

use crate::{
	cn,
	uikit::{
		ButtonVariant, INPUT_BASE, INPUT_GROUP_ADDON_BASE, INPUT_GROUP_BASE, INPUT_GROUP_BUTTON_BASE, INPUT_GROUP_INPUT_CONTROL, INPUT_GROUP_TEXT, INPUT_GROUP_TEXTAREA_CONTROL,
		InputGroupAddonAlign, InputGroupButtonSize, TEXTAREA_BASE, button::Button, input_group_button_size_class,
	},
};

#[component]
pub fn InputGroup(#[props(default)] class: String, children: Element) -> Element {
	let cls = cn!(INPUT_GROUP_BASE, class);
	rsx! {
		div { class: cls, "data-slot": "input-group", role: "group", {children} }
	}
}

#[component]
pub fn InputGroupAddon(#[props(default)] align: InputGroupAddonAlign, #[props(default)] class: String, children: Element) -> Element {
	let cls = cn!(INPUT_GROUP_ADDON_BASE, align.as_class(), class);
	rsx! {
		div {
			role: "group",
			class: cls,
			"data-slot": "input-group-addon",
			"data-align": align.as_ref(),
			{children}
		}
	}
}

#[component]
pub fn InputGroupButton(#[props(default)] size: InputGroupButtonSize, #[props(default)] icon: bool, #[props(default)] class: String, children: Element) -> Element {
	let cls = cn!(INPUT_GROUP_BUTTON_BASE, input_group_button_size_class(size, icon), class);
	rsx! {
		Button {
			variant: ButtonVariant::Ghost,
			class: cls,
			{children}
		}
	}
}

#[component]
pub fn InputGroupText(#[props(default)] class: String, children: Element) -> Element {
	let cls = cn!(INPUT_GROUP_TEXT, class);
	rsx! {
		span { class: cls, {children} }
	}
}

#[component]
pub fn InputGroupInput(
	#[props(default)] class: String,
	#[props(default)] r#type: String,
	#[props(default)] placeholder: String,
	#[props(default)] disabled: bool,
	#[props(default)] value: String,
	oninput: Option<EventHandler<FormEvent>>,
) -> Element {
	let cls = cn!(INPUT_BASE, INPUT_GROUP_INPUT_CONTROL, class);
	let input_type = if r#type.is_empty() { "text".to_string() } else { r#type };
	rsx! {
		input {
			r#type: input_type,
			class: cls,
			"data-slot": "input-group-control",
			placeholder,
			disabled,
			value,
			oninput: move |e| { if let Some(h) = oninput { h.call(e); } },
		}
	}
}

#[component]
pub fn InputGroupTextarea(
	#[props(default)] class: String,
	#[props(default)] placeholder: String,
	#[props(default)] disabled: bool,
	#[props(default)] value: String,
	oninput: Option<EventHandler<FormEvent>>,
) -> Element {
	let cls = cn!(TEXTAREA_BASE, INPUT_GROUP_TEXTAREA_CONTROL, class);
	rsx! {
		textarea {
			class: cls,
			"data-slot": "input-group-control",
			placeholder,
			disabled,
			value,
			oninput: move |e| { if let Some(h) = oninput { h.call(e); } },
		}
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::uikit::test_util::render;

	#[test]
	fn group_renders_base_slot_and_role() {
		fn app() -> Element {
			rsx! {
				InputGroup { "x" }
			}
		}
		let html = render(app);
		assert!(html.contains("data-slot=\"input-group\""), "{html}");
		assert!(html.contains("role=\"group\""), "{html}");
		assert!(html.contains("group/input-group"), "{html}");
	}

	#[test]
	fn group_drops_dark_classes() {
		fn app() -> Element {
			rsx! {
				InputGroup { "x" }
			}
		}
		let html = render(app);
		assert!(!html.contains("dark:"), "dark variants must be dropped: {html}");
	}

	#[test]
	fn addon_default_align_is_inline_start() {
		fn app() -> Element {
			rsx! {
				InputGroupAddon { "a" }
			}
		}
		let html = render(app);
		assert!(html.contains("data-slot=\"input-group-addon\""), "{html}");
		assert!(html.contains("data-align=\"inline-start\""), "{html}");
		assert!(html.contains("order-first"), "{html}");
	}

	#[test]
	fn addon_block_end_align_is_canon() {
		fn app() -> Element {
			rsx! {
				InputGroupAddon { align: InputGroupAddonAlign::BlockEnd, "a" }
			}
		}
		let html = render(app);
		assert!(html.contains("data-align=\"block-end\""), "{html}");
		assert!(html.contains("order-last"), "{html}");
	}

	#[test]
	fn button_default_size_and_ghost_variant() {
		fn app() -> Element {
			rsx! {
				InputGroupButton { "b" }
			}
		}
		let html = render(app);
		assert!(html.contains("data-slot=\"button\""), "{html}");
		assert!(html.contains("hover:bg-accent"), "ghost variant: {html}");
		assert!(html.contains("h-6"), "xs size class: {html}");
	}

	#[test]
	fn icon_button_squares_and_drops_padding() {
		fn app() -> Element {
			rsx! {
				InputGroupButton { size: InputGroupButtonSize::Sm, icon: true, "b" }
			}
		}
		let html = render(app);
		assert!(html.contains("size-8"), "{html}");
		assert!(html.contains("p-0"), "icon button folds to a square, padding zeroed: {html}");
	}

	#[test]
	fn addon_align_serializes_kebab() {
		assert_eq!(InputGroupAddonAlign::InlineStart.as_ref(), "inline-start");
	}

	#[test]
	fn control_drops_dark_classes() {
		fn app() -> Element {
			rsx! {
				InputGroupInput {}
				InputGroupTextarea {}
			}
		}
		let html = render(app);
		assert!(html.contains("data-slot=\"input-group-control\""), "{html}");
		assert!(html.contains("focus-visible:ring-0"), "{html}");
		assert!(!html.contains("dark:"), "dark variants must be dropped: {html}");
	}
}