fc_dioxus/components/
avatar.rs1use crate::{
3 core::{assets, attrs::component_attributes},
4 prelude::*,
5 theme::use_stylesheets,
6};
7const BEM: Bem = Bem::new("fc-avatar");
8const GROUP: Bem = Bem::new("fc-avatar-group");
9fc_modifier! {
10 pub enum AvatarSize for BEM {
12 Md => None,
14 Xs => "xs",
16 Sm => "sm",
18 Lg => "lg",
20 Xl => "xl",
22 }
23}
24fc_modifier! { enum AvatarShape for BEM { Round => None, Square => "square" } }
25fc_modifier! { enum AvatarTone for BEM { Accent => None, Neutral => "neutral" } }
26
27#[must_use]
32pub fn initials(name: &str, count: usize) -> String {
33 let words: Vec<&str> = name.split_whitespace().collect();
34 let first = words.first().and_then(|w| w.chars().next());
35 let last = if words.len() > 1 {
36 words.last().and_then(|w| w.chars().next())
37 } else {
38 None
39 };
40 [first, last]
41 .into_iter()
42 .flatten()
43 .take(count)
44 .flat_map(char::to_uppercase)
45 .collect()
46}
47
48#[component]
54pub fn Avatar(
55 name: String,
56 #[props(default)] src: Option<String>,
57 #[props(default)] alt: Option<Alt>,
58 #[props(default)]
60 initials: Option<String>,
61 #[props(default)] size: AvatarSize,
62 #[props(default)] square: bool,
63 #[props(default)] neutral: bool,
64 #[props(default)] class: Option<String>,
65 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
66) -> Element {
67 let classes = ClassList::new(BEM.block())
68 .with_modifier(BEM, size)
69 .with_modifier(
70 BEM,
71 if square {
72 AvatarShape::Square
73 } else {
74 AvatarShape::Round
75 },
76 )
77 .with_modifier(
78 BEM,
79 if neutral {
80 AvatarTone::Neutral
81 } else {
82 AvatarTone::Accent
83 },
84 )
85 .extend(class.as_deref());
86 let mut attributes =
87 component_attributes(attributes, classes, &["role", "aria-label", "aria-hidden"]);
88 let label = match alt {
89 Some(Alt::Text(name)) => Some(name.to_string()),
90 Some(Alt::Decorative) => None,
91 None => AccessibleName::try_from(name.as_str())
92 .ok()
93 .map(|n| n.to_string()),
94 };
95 let text = initials
96 .unwrap_or_else(|| self::initials(&name, if size == AvatarSize::Xs { 1 } else { 2 }));
97 match (&src, &label) {
98 (Some(_), _) => {}
100 (None, Some(label)) => {
101 attributes.push(Attribute::new("role", "img", None, false));
102 attributes.push(Attribute::new("aria-label", label.clone(), None, false));
103 }
104 (None, None) => attributes.push(Attribute::new("aria-hidden", "true", None, false)),
105 }
106 rsx! {
107 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_AVATAR_CSS } }
108 span { ..attributes,
109 if let Some(src) = src {
110 img { class: BEM.element("image"), src, alt: label.unwrap_or_default() }
111 } else {
112 "{text}"
113 }
114 }
115 }
116}
117
118#[component]
123pub fn AvatarGroup(
124 #[props(default)]
126 label: Option<AccessibleName>,
127 #[props(default)]
129 overflow: u32,
130 #[props(default)]
132 size: AvatarSize,
133 #[props(default)] class: Option<String>,
134 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
135 children: Element,
136) -> Element {
137 let mut attributes = component_attributes(
138 attributes,
139 ClassList::new(GROUP.block()).extend(class.as_deref()),
140 &["role", "aria-label"],
141 );
142 if let Some(label) = label {
143 attributes.push(Attribute::new("role", "group", None, false));
144 attributes.push(Attribute::new("aria-label", label.to_string(), None, false));
145 }
146 let more = format!("{overflow} more");
147 rsx! {
148 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_AVATAR_CSS } }
149 span { ..attributes,
150 {children}
151 if overflow > 0 {
152 Avatar { name: more, alt: Alt::Decorative, initials: format!("+{overflow}"), size, neutral: true }
153 }
154 }
155 }
156}