diff --git a/src/Components/DataDisplay/SUIAvatar.razor b/src/Components/DataDisplay/SUIAvatar.razor index f4a194c..58a3255 100644 --- a/src/Components/DataDisplay/SUIAvatar.razor +++ b/src/Components/DataDisplay/SUIAvatar.razor @@ -1,8 +1,13 @@ @namespace Sufficit.Blazor.UI.Components -@if (!string.IsNullOrWhiteSpace(Src)) +@if (!_imageLoadFailed && !string.IsNullOrWhiteSpace(Src)) { - @Alt + @Alt } else { @@ -55,6 +60,20 @@ else [Parameter(CaptureUnmatchedValues = true)] public Dictionary AdditionalAttributes { get; set; } = new(); + private string? _renderedSrc; + private bool _imageLoadFailed; + + protected override void OnParametersSet() + { + if (!string.Equals(_renderedSrc, Src, StringComparison.Ordinal)) + { + _renderedSrc = Src; + _imageLoadFailed = false; + } + } + + private void HandleImageError() => _imageLoadFailed = true; + /// /// Size suffix. The stylesheet already used --sm/--md/--lg for the avatars /// rendered inside SUINavGroup and SUINavLink, so this maps onto the diff --git a/tests/Sufficit.Blazor.UI.Tests/ComponentLifecycleTests.cs b/tests/Sufficit.Blazor.UI.Tests/ComponentLifecycleTests.cs index 84b3814..4baae64 100644 --- a/tests/Sufficit.Blazor.UI.Tests/ComponentLifecycleTests.cs +++ b/tests/Sufficit.Blazor.UI.Tests/ComponentLifecycleTests.cs @@ -58,6 +58,40 @@ public void CardHeader_SupportsLegacyNamedSlotsDuringSuiMigration() Assert.NotNull(cut.Find("[data-slot='actions']")); } + [Fact] + public void Avatar_FallsBackToChildContentWhenImageFails() + { + using var context = new BunitContext(); + var cut = context.Render(parameters => parameters + .Add(component => component.Src, "https://avatars.tests.local/owner.jpg") + .Add(component => component.Alt, string.Empty) + .AddChildContent("OP")); + + cut.Find("img").TriggerEvent("onerror", new EventArgs()); + + var fallback = cut.Find("div.sui-avatar"); + Assert.Equal("OP", fallback.TextContent); + Assert.Equal("true", fallback.GetAttribute("aria-hidden")); + } + + [Fact] + public void Avatar_RetriesWhenItsSourceChanges() + { + using var context = new BunitContext(); + var cut = context.Render(parameters => parameters + .Add(component => component.Src, "https://avatars.tests.local/first.jpg") + .AddChildContent("OP")); + cut.Find("img").TriggerEvent("onerror", new EventArgs()); + + cut.Render(parameters => parameters + .Add(component => component.Src, "https://avatars.tests.local/second.jpg") + .AddChildContent("OP")); + + Assert.Equal( + "https://avatars.tests.local/second.jpg", + cut.Find("img").GetAttribute("src")); + } + [Fact] public async Task DialogHost_CompletesReplacedBackdropAndDisposedRequests() {