use crate::{
core::{assets, attrs::component_attributes},
prelude::*,
theme::use_stylesheets,
};
const BEM: Bem = Bem::new("fc-avatar");
const GROUP: Bem = Bem::new("fc-avatar-group");
fc_modifier! {
pub enum AvatarSize for BEM {
Md => None,
Xs => "xs",
Sm => "sm",
Lg => "lg",
Xl => "xl",
}
}
fc_modifier! { enum AvatarShape for BEM { Round => None, Square => "square" } }
fc_modifier! { enum AvatarTone for BEM { Accent => None, Neutral => "neutral" } }
#[must_use]
pub fn initials(name: &str, count: usize) -> String {
let words: Vec<&str> = name.split_whitespace().collect();
let first = words.first().and_then(|w| w.chars().next());
let last = if words.len() > 1 {
words.last().and_then(|w| w.chars().next())
} else {
None
};
[first, last]
.into_iter()
.flatten()
.take(count)
.flat_map(char::to_uppercase)
.collect()
}
#[component]
pub fn Avatar(
name: String,
#[props(default)] src: Option<String>,
#[props(default)] alt: Option<Alt>,
#[props(default)]
initials: Option<String>,
#[props(default)] size: AvatarSize,
#[props(default)] square: bool,
#[props(default)] neutral: bool,
#[props(default)] class: Option<String>,
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
) -> Element {
let classes = ClassList::new(BEM.block())
.with_modifier(BEM, size)
.with_modifier(
BEM,
if square {
AvatarShape::Square
} else {
AvatarShape::Round
},
)
.with_modifier(
BEM,
if neutral {
AvatarTone::Neutral
} else {
AvatarTone::Accent
},
)
.extend(class.as_deref());
let mut attributes =
component_attributes(attributes, classes, &["role", "aria-label", "aria-hidden"]);
let label = match alt {
Some(Alt::Text(name)) => Some(name.to_string()),
Some(Alt::Decorative) => None,
None => AccessibleName::try_from(name.as_str())
.ok()
.map(|n| n.to_string()),
};
let text = initials
.unwrap_or_else(|| self::initials(&name, if size == AvatarSize::Xs { 1 } else { 2 }));
match (&src, &label) {
(Some(_), _) => {}
(None, Some(label)) => {
attributes.push(Attribute::new("role", "img", None, false));
attributes.push(Attribute::new("aria-label", label.clone(), None, false));
}
(None, None) => attributes.push(Attribute::new("aria-hidden", "true", None, false)),
}
rsx! {
if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_AVATAR_CSS } }
span { ..attributes,
if let Some(src) = src {
img { class: BEM.element("image"), src, alt: label.unwrap_or_default() }
} else {
"{text}"
}
}
}
}
#[component]
pub fn AvatarGroup(
#[props(default)]
label: Option<AccessibleName>,
#[props(default)]
overflow: u32,
#[props(default)]
size: AvatarSize,
#[props(default)] class: Option<String>,
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let mut attributes = component_attributes(
attributes,
ClassList::new(GROUP.block()).extend(class.as_deref()),
&["role", "aria-label"],
);
if let Some(label) = label {
attributes.push(Attribute::new("role", "group", None, false));
attributes.push(Attribute::new("aria-label", label.to_string(), None, false));
}
let more = format!("{overflow} more");
rsx! {
if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_AVATAR_CSS } }
span { ..attributes,
{children}
if overflow > 0 {
Avatar { name: more, alt: Alt::Decorative, initials: format!("+{overflow}"), size, neutral: true }
}
}
}
}