Skip to content

Commit 442321f

Browse files
authored
[Bug Fix] Add Avatar image fallback handling (#387)
* [Bug Fix] Add Avatar image fallback handling (#259) * fix(avatar): preserve image visibility without JS Image was hidden by default and only revealed by the Stimulus controller's load handler, which meant clients with JS disabled never saw the avatar image. Make the image visible by default and let the controller hide it on connect when it isn't yet loaded; load/error events restore the proper image/fallback toggle.
1 parent a2153ef commit 442321f

7 files changed

Lines changed: 81 additions & 0 deletions

File tree

docs/app/javascript/controllers/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ application.register("ruby-ui--accordion", RubyUi__AccordionController)
1313
import RubyUi__AlertDialogController from "./ruby_ui/alert_dialog_controller"
1414
application.register("ruby-ui--alert-dialog", RubyUi__AlertDialogController)
1515

16+
import RubyUi__AvatarController from "./ruby_ui/avatar_controller"
17+
application.register("ruby-ui--avatar", RubyUi__AvatarController)
18+
1619
import RubyUi__CalendarController from "./ruby_ui/calendar_controller"
1720
application.register("ruby-ui--calendar", RubyUi__CalendarController)
1821

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Controller } from "@hotwired/stimulus";
2+
3+
export default class extends Controller {
4+
static targets = ["image", "fallback"];
5+
6+
connect() {
7+
if (!this.hasImageTarget) return;
8+
9+
if (this.imageTarget.complete && this.imageTarget.naturalWidth > 0) {
10+
this.showImage();
11+
} else {
12+
this.showFallback();
13+
}
14+
}
15+
16+
showImage() {
17+
this.imageTargets.forEach((image) => image.classList.remove("hidden"));
18+
this.fallbackTargets.forEach((fallback) =>
19+
fallback.classList.add("hidden"),
20+
);
21+
}
22+
23+
showFallback() {
24+
this.imageTargets.forEach((image) => image.classList.add("hidden"));
25+
this.fallbackTargets.forEach((fallback) =>
26+
fallback.classList.remove("hidden"),
27+
);
28+
}
29+
}

gem/lib/ruby_ui/avatar/avatar.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ def view_template(&)
2424

2525
def default_attrs
2626
{
27+
data: {
28+
controller: "ruby-ui--avatar"
29+
},
2730
class: ["relative flex shrink-0 overflow-hidden rounded-full", @size_classes]
2831
}
2932
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Controller } from "@hotwired/stimulus";
2+
3+
export default class extends Controller {
4+
static targets = ["image", "fallback"];
5+
6+
connect() {
7+
if (!this.hasImageTarget) {
8+
return;
9+
}
10+
11+
if (this.imageTarget.complete && this.imageTarget.naturalWidth > 0) {
12+
this.showImage();
13+
} else {
14+
// Image not yet loaded (or failed): hide it so the fallback shows.
15+
// Image visibility is restored by the load/error handlers.
16+
this.showFallback();
17+
}
18+
}
19+
20+
showImage() {
21+
this.imageTargets.forEach((image) => image.classList.remove("hidden"));
22+
this.fallbackTargets.forEach((fallback) =>
23+
fallback.classList.add("hidden"),
24+
);
25+
}
26+
27+
showFallback() {
28+
this.imageTargets.forEach((image) => image.classList.add("hidden"));
29+
this.fallbackTargets.forEach((fallback) =>
30+
fallback.classList.remove("hidden"),
31+
);
32+
}
33+
}

gem/lib/ruby_ui/avatar/avatar_fallback.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ def view_template(&)
1010

1111
def default_attrs
1212
{
13+
data: {
14+
ruby_ui__avatar_target: "fallback"
15+
},
1316
class: "flex h-full w-full items-center justify-center rounded-full bg-muted"
1417
}
1518
end

gem/lib/ruby_ui/avatar/avatar_image.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ def view_template
1717
def default_attrs
1818
{
1919
loading: "lazy",
20+
data: {
21+
ruby_ui__avatar_target: "image",
22+
action: "load->ruby-ui--avatar#showImage error->ruby-ui--avatar#showFallback"
23+
},
2024
class: "aspect-square h-full w-full",
2125
alt: @alt,
2226
src: @src

gem/test/ruby_ui/avatar_test.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,11 @@ def test_render_with_all_items
1212
end
1313

1414
assert_match(/joeldrapper/, output)
15+
assert_match(/data-controller="ruby-ui--avatar"/, output)
16+
assert_match(/data-ruby-ui--avatar-target="image"/, output)
17+
assert_match(/load->ruby-ui--avatar#showImage error->ruby-ui--avatar#showFallback/, output)
18+
assert_match(/data-ruby-ui--avatar-target="fallback"/, output)
19+
assert_match(/class="aspect-square h-full w-full"/, output)
20+
refute_match(/class="[^"]*\bhidden\b[^"]*aspect-square/, output)
1521
end
1622
end

0 commit comments

Comments
 (0)