#![doc = include_str!("../DIOXUS.md")]
use crate::common::{
Color, ImageLoadingStatus, Overlap, Size, Variant, base_container_style, base_fallback_style,
base_group_grid_style, base_group_style, child_overlap_style,
};
use dioxus::prelude::*;
use image_rs::common::{
AriaLive, AriaPressed, CrossOrigin, Decoding, FetchPriority, Layout, Loading, ObjectFit,
Position, ReferrerPolicy,
};
use image_rs::dioxus::Image as ImageRS;
use std::time::Duration;
#[derive(Clone, Debug, PartialEq)]
pub struct GroupContext {
pub size: Size,
pub color: Color,
pub variant: Variant,
pub overlap: Overlap,
pub is_grid: bool,
pub overflow_count: usize,
}
#[derive(Props, Clone, PartialEq)]
pub struct GroupProps {
#[props(default)]
pub children: Element,
#[props(default)]
pub max: Option<usize>,
#[props(default)]
pub total: Option<usize>,
#[props(default)]
pub size: Size,
#[props(default)]
pub color: Color,
#[props(default)]
pub variant: Variant,
#[props(default)]
pub overlap: Overlap,
#[props(default)]
pub is_grid: bool,
#[props(default)]
pub class: &'static str,
#[props(default)]
pub style: &'static str,
#[props(default)]
pub id: &'static str,
#[props(default = "Avatar group")]
pub aria_label: &'static str,
#[props(default = "group")]
pub role: &'static str,
#[props(default = "0")]
pub tabindex: &'static str,
#[props(default)]
pub data_testid: &'static str,
}
#[component]
pub fn Group(props: GroupProps) -> Element {
let overflow_count = match (props.max, props.total) {
(Some(max), Some(total)) => total.saturating_sub(max),
_ => 0,
};
let ctx = GroupContext {
size: props.size,
color: props.color,
variant: props.variant,
overlap: props.overlap,
is_grid: props.is_grid,
overflow_count,
};
use_context_provider(|| ctx);
let base_style = if props.is_grid {
base_group_grid_style()
} else {
base_group_style()
};
let group_class = if props.is_grid {
"avatar-group avatar-group--grid"
} else {
match props.overlap {
Overlap::Clip => "avatar-group avatar-group--clip",
Overlap::Ring => "avatar-group avatar-group--ring",
}
};
rsx! {
div {
id: props.id,
class: "{group_class} {props.class}",
style: "{base_style} {props.style}",
role: props.role,
aria_label: props.aria_label,
tabindex: props.tabindex,
"data-testid": props.data_testid,
{props.children}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct CountProps {
#[props(default)]
pub children: Option<Element>,
#[props(default)]
pub count: usize,
#[props(default = None)]
pub size: Option<Size>,
#[props(default = None)]
pub color: Option<Color>,
#[props(default = None)]
pub variant: Option<Variant>,
#[props(default)]
pub class: &'static str,
#[props(default)]
pub style: &'static str,
#[props(default)]
pub id: &'static str,
#[props(default = "Additional members")]
pub aria_label: &'static str,
}
#[component]
pub fn Count(props: CountProps) -> Element {
let group = resolve_group_context();
let (group_size, group_color, group_variant, ctx_overflow) = group
.map(|ctx| (ctx.size, ctx.color, ctx.variant, ctx.overflow_count))
.unwrap_or((Size::Md, Color::Default, Variant::Default, 0));
let size = props.size.unwrap_or(group_size);
let color = props.color.unwrap_or(group_color);
let variant = props.variant.unwrap_or(group_variant);
let effective_count = if props.count == 0 {
ctx_overflow
} else {
props.count
};
let content = match props.children {
Some(c) => rsx! { {c} },
None => rsx! { "+{effective_count}" },
};
rsx! {
Avatar {
size: size,
container_class: "avatar-group__count",
class: props.class,
style: props.style,
id: props.id,
aria_label: props.aria_label,
Fallback {
color: color,
variant: variant,
{content}
}
}
}
}
fn resolve_group_context() -> Option<GroupContext> {
try_consume_context::<GroupContext>()
}
#[derive(Props, Clone, PartialEq)]
pub struct AvatarProps {
#[props(default)]
pub children: Element,
#[props(default)]
pub size: Option<Size>,
#[props(default)]
pub class: &'static str,
#[props(default)]
pub style: &'static str,
#[props(default)]
pub id: &'static str,
#[props(default = "Avatar")]
pub aria_label: &'static str,
#[props(default = "0")]
pub tabindex: &'static str,
#[props(default = "avatar")]
pub container_class: &'static str,
#[props(default)]
pub container_style: &'static str,
#[props(default)]
pub data_testid: &'static str,
}
#[component]
pub fn Avatar(props: AvatarProps) -> Element {
let group_ctx = resolve_group_context();
let status = use_signal(|| ImageLoadingStatus::Idle);
use_context_provider(|| status);
let computed_size = props
.size
.unwrap_or_else(|| group_ctx.as_ref().map(|ctx| ctx.size).unwrap_or(Size::Md));
let overlap_style = match &group_ctx {
Some(ctx) if !ctx.is_grid => child_overlap_style(ctx.overlap, computed_size),
_ => "",
};
rsx! {
span {
id: props.id,
class: "{props.container_class} {props.class}",
style: "{base_container_style()} {computed_size.to_style()} {computed_size.to_font_style()} {overlap_style} {props.style} {props.container_style}",
role: "img",
aria_label: props.aria_label,
tabindex: props.tabindex,
"data-testid": props.data_testid,
{props.children}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct ImageProps {
#[props(default)]
pub src: &'static str,
#[props(default)]
pub fallback_src: &'static str,
#[props(default)]
pub srcset: &'static str,
#[props(default)]
pub sizes: &'static str,
#[props(default)]
pub alt: &'static str,
#[props(default = "avatar__image")]
pub class: &'static str,
#[props(default)]
pub style: &'static str,
#[props(default)]
pub id: &'static str,
#[props(default)]
pub loading: Loading,
#[props(default)]
pub crossorigin: CrossOrigin,
#[props(default)]
pub decoding: Decoding,
#[props(default)]
pub referrerpolicy: ReferrerPolicy,
#[props(default)]
pub fetchpriority: FetchPriority,
#[props(default)]
pub object_fit: ObjectFit,
#[props(default)]
pub object_position: Position,
#[props(default)]
pub layout: Layout,
#[props(default)]
pub quality: &'static str,
#[props(default)]
pub placeholder: &'static str,
#[props(default)]
pub blur_data_url: &'static str,
#[props(default)]
pub elementtiming: &'static str,
#[props(default)]
pub aria_live: AriaLive,
#[props(default)]
pub aria_pressed: AriaPressed,
#[props(default)]
pub aria_controls: &'static str,
#[props(default)]
pub aria_labelledby: &'static str,
#[props(default)]
pub aria_describedby: &'static str,
#[props(default)]
pub aria_expanded: &'static str,
#[props(default)]
pub width: &'static str,
#[props(default)]
pub height: &'static str,
#[props(default)]
pub on_load: Option<Callback<()>>,
#[props(default)]
pub on_error: Option<Callback<String>>,
}
#[component]
pub fn Image(props: ImageProps) -> Element {
let mut status = use_context::<Signal<ImageLoadingStatus>>();
use_effect(move || {
if !props.src.is_empty() {
status.set(ImageLoadingStatus::Loading);
}
});
let on_load = move |()| {
status.set(ImageLoadingStatus::Loaded);
if let Some(cb) = &props.on_load {
cb.call(());
}
};
let on_error = move |err: String| {
status.set(ImageLoadingStatus::Error);
if let Some(cb) = &props.on_error {
cb.call(err);
}
};
let is_loaded = status() == ImageLoadingStatus::Loaded;
rsx! {
span {
id: props.id,
class: props.class,
style: if is_loaded { "width: 100%; height: 100%; display: block;" } else { "display: none;" },
aria_hidden: if is_loaded { "false" } else { "true" },
ImageRS {
src: props.src,
alt: props.alt,
width: props.width,
height: props.height,
layout: props.layout,
fallback_src: props.fallback_src,
srcset: props.srcset,
sizes: props.sizes,
style: "width: 100%; height: 100%; object-fit: cover; border-radius: inherit;",
loading: props.loading,
crossorigin: props.crossorigin,
decoding: props.decoding,
referrerpolicy: props.referrerpolicy,
fetchpriority: props.fetchpriority,
object_fit: props.object_fit,
object_position: props.object_position,
quality: props.quality,
placeholder: props.placeholder,
blur_data_url: props.blur_data_url,
elementtiming: props.elementtiming,
aria_live: props.aria_live,
aria_pressed: props.aria_pressed,
aria_controls: props.aria_controls,
aria_labelledby: props.aria_labelledby,
aria_describedby: props.aria_describedby,
aria_expanded: props.aria_expanded,
on_load: on_load,
on_error: on_error,
}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct FallbackProps {
#[props(default)]
pub children: Element,
#[props(default)]
pub delay_ms: u32,
#[props(default = Color::Default)]
pub color: Color,
#[props(default = Variant::Default)]
pub variant: Variant,
#[props(default = "avatar__fallback")]
pub class: &'static str,
#[props(default)]
pub style: &'static str,
#[props(default)]
pub id: &'static str,
#[props(default)]
pub data_testid: &'static str,
}
#[component]
pub fn Fallback(props: FallbackProps) -> Element {
let status = use_context::<Signal<ImageLoadingStatus>>();
let group = resolve_group_context();
let mut visible = use_signal(|| props.delay_ms == 0);
use_effect(move || {
if props.delay_ms > 0 {
let delay = props.delay_ms;
spawn(async move {
platform_sleep(Duration::from_millis(u64::from(delay))).await;
visible.set(true);
});
}
});
let image_loaded = status() == ImageLoadingStatus::Loaded;
let should_show = visible() && !image_loaded;
if !should_show {
return rsx! {};
}
let (group_color, group_variant) = group
.map(|ctx| (ctx.color, ctx.variant))
.unwrap_or((Color::Default, Variant::Default));
let effective_color = if props.color == Color::Default {
group_color
} else {
props.color
};
let effective_variant = if props.variant == Variant::Default {
group_variant
} else {
props.variant
};
let color_style = match effective_variant {
Variant::Soft => effective_color.to_soft_style(),
Variant::Default => effective_color.to_style(),
};
rsx! {
span {
id: props.id,
class: props.class,
style: "{base_fallback_style()} {color_style} {props.style}",
aria_hidden: if image_loaded { "true" } else { "false" },
"data-testid": props.data_testid,
{props.children}
}
}
}
async fn platform_sleep(_duration: Duration) {
std::future::ready(()).await
}