yggdrasil/components/ui/
icons.rs1use leptos::prelude::*;
6
7#[path = "icons_data.rs"]
8mod data;
9
10pub use data::IconName;
11
12#[component]
17pub fn Icon(
18 name: IconName,
19 #[prop(default = 16)] size: u32,
20 #[prop(optional, into)] class: String,
21 #[prop(optional, into)] label: Option<String>,
22) -> impl IntoView {
23 let body = name.body();
24 let slug = name.slug();
25 let class = format!("lucide lucide-{slug} shrink-0 {class}");
26 let size = size.to_string();
27 match label {
28 Some(label) => view! {
29 <svg
30 class=class
31 width=size.clone()
32 height=size
33 viewBox="0 0 24 24"
34 fill="none"
35 stroke="currentColor"
36 stroke-width="1.5"
37 stroke-linecap="round"
38 stroke-linejoin="round"
39 role="img"
40 aria-label=label
41 inner_html=body
42 ></svg>
43 }
44 .into_any(),
45 None => view! {
46 <svg
47 class=class
48 width=size.clone()
49 height=size
50 viewBox="0 0 24 24"
51 fill="none"
52 stroke="currentColor"
53 stroke-width="1.5"
54 stroke-linecap="round"
55 stroke-linejoin="round"
56 aria-hidden="true"
57 focusable="false"
58 inner_html=body
59 ></svg>
60 }
61 .into_any(),
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn every_icon_has_drawable_body() {
71 for icon in IconName::ALL {
72 let body = icon.body();
73 assert!(body.starts_with('<'), "{} 没有图形", icon.slug());
74 assert!(!body.contains("<script"), "{} 含脚本", icon.slug());
75 assert!(!body.contains("stroke-width"), "{} 自带线宽", icon.slug());
76 }
77 }
78
79 #[test]
80 fn slugs_are_unique() {
81 let mut slugs: Vec<_> = IconName::ALL.iter().map(|i| i.slug()).collect();
82 slugs.sort_unstable();
83 slugs.dedup();
84 assert_eq!(slugs.len(), IconName::ALL.len());
85 }
86
87 #[test]
88 fn decorative_icons_are_hidden_from_assistive_tech() {
89 let html = Owner::new().with(|| view! { <Icon name=IconName::Search/> }.to_html());
90 assert!(html.contains("aria-hidden=\"true\""), "{html}");
91 assert!(html.contains("stroke-width=\"1.5\""), "{html}");
92 assert!(html.contains("lucide-search"), "{html}");
93 assert!(html.contains("<circle"), "{html}");
94 }
95
96 #[test]
97 fn labelled_icons_are_announced() {
98 let html =
99 Owner::new().with(|| view! { <Icon name=IconName::X size=20 label="关闭"/> }.to_html());
100 assert!(html.contains("role=\"img\""), "{html}");
101 assert!(html.contains("aria-label=\"关闭\""), "{html}");
102 assert!(html.contains("width=\"20\""), "{html}");
103 assert!(!html.contains("aria-hidden"), "{html}");
104 }
105}