Skip to content

Commit 1752abd

Browse files
committed
[Bug Fix] Toast review polish: swap promise icon, drop dead code, more tests
Issues found and fixed during implementation review: - _mutate (promise transitions) now swaps the icon SVG to match the resolved variant, and updates aria role for error case. Previously loading -> success kept the spinner. - Drop unused dataset bracket-key assignment in _spawn (only the setAttribute path was effective). - _dismissById iterates the tracked _items list rather than raw OL children (defensive against non-toast nodes). - Drop unused POSITIONS constant from ToastRegion. - Add Minitest coverage for ToastAction label/data-action, ToastCancel label, ToastClose dismiss action + sr-only, ToastIcon per-variant SVG content, ToastTitle/Description slots. 181 runs, 787 assertions, 0 failures.
1 parent 653e120 commit 1752abd

4 files changed

Lines changed: 74 additions & 7 deletions

File tree

docs/app/javascript/controllers/ruby_ui/toaster_controller.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ export default class extends Controller {
8282
node.id = detail.id || `toast-${this._uuid()}`
8383
if (detail.duration != null) {
8484
const dur = detail.duration === Infinity ? 0 : detail.duration
85-
node.dataset["rubyUi--ToastDurationValue"] = String(dur)
8685
node.setAttribute("data-ruby-ui--toast-duration-value", String(dur))
8786
}
8887
if (detail.dismissible === false) {
@@ -128,8 +127,8 @@ export default class extends Controller {
128127

129128
_dismissById(id) {
130129
if (!id) {
131-
Array.from(this._listEl.children).forEach((c) =>
132-
c.dispatchEvent(new CustomEvent("ruby-ui:toast:force-dismiss", { bubbles: true }))
130+
this._items.slice().forEach((el) =>
131+
el.dispatchEvent(new CustomEvent("ruby-ui:toast:force-dismiss", { bubbles: true }))
133132
)
134133
return
135134
}
@@ -271,13 +270,24 @@ export default class extends Controller {
271270
const el = this._listEl.querySelector(`#${CSS.escape(id)}`)
272271
if (!el) return
273272
el.dataset.variant = variant
273+
el.setAttribute("role", variant === "error" ? "alert" : "status")
274+
this._swapIcon(el, variant)
274275
const t = el.querySelector('[data-slot="title"]')
275276
if (t && text) t.textContent = text
276277
const dur = String(this.durationValue)
277278
el.setAttribute("data-ruby-ui--toast-duration-value", dur)
278279
el.dispatchEvent(new CustomEvent("ruby-ui:toast:restart", { bubbles: true }))
279280
}
280281

282+
_swapIcon(el, variant) {
283+
const iconHost = el.querySelector('[data-slot="icon"]')
284+
if (!iconHost) return
285+
const tpl = this._skeletonFor(variant)
286+
if (!tpl) return
287+
const sourceIcon = tpl.content.firstElementChild?.querySelector('[data-slot="icon"]')
288+
iconHost.innerHTML = sourceIcon ? sourceIcon.innerHTML : ""
289+
}
290+
281291
_uuid() {
282292
if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID()
283293
return Math.random().toString(36).slice(2) + Date.now().toString(36)

gem/lib/ruby_ui/toast/toast_region.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
module RubyUI
44
class ToastRegion < Base
55
SKELETON_VARIANTS = %i[default success error warning info loading].freeze
6-
POSITIONS = %i[top_left top_center top_right bottom_left bottom_center bottom_right].freeze
76

87
def initialize(
98
position: :bottom_right,

gem/lib/ruby_ui/toast/toaster_controller.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ export default class extends Controller {
8282
node.id = detail.id || `toast-${this._uuid()}`
8383
if (detail.duration != null) {
8484
const dur = detail.duration === Infinity ? 0 : detail.duration
85-
node.dataset["rubyUi--ToastDurationValue"] = String(dur)
8685
node.setAttribute("data-ruby-ui--toast-duration-value", String(dur))
8786
}
8887
if (detail.dismissible === false) {
@@ -128,8 +127,8 @@ export default class extends Controller {
128127

129128
_dismissById(id) {
130129
if (!id) {
131-
Array.from(this._listEl.children).forEach((c) =>
132-
c.dispatchEvent(new CustomEvent("ruby-ui:toast:force-dismiss", { bubbles: true }))
130+
this._items.slice().forEach((el) =>
131+
el.dispatchEvent(new CustomEvent("ruby-ui:toast:force-dismiss", { bubbles: true }))
133132
)
134133
return
135134
}
@@ -271,13 +270,24 @@ export default class extends Controller {
271270
const el = this._listEl.querySelector(`#${CSS.escape(id)}`)
272271
if (!el) return
273272
el.dataset.variant = variant
273+
el.setAttribute("role", variant === "error" ? "alert" : "status")
274+
this._swapIcon(el, variant)
274275
const t = el.querySelector('[data-slot="title"]')
275276
if (t && text) t.textContent = text
276277
const dur = String(this.durationValue)
277278
el.setAttribute("data-ruby-ui--toast-duration-value", dur)
278279
el.dispatchEvent(new CustomEvent("ruby-ui:toast:restart", { bubbles: true }))
279280
}
280281

282+
_swapIcon(el, variant) {
283+
const iconHost = el.querySelector('[data-slot="icon"]')
284+
if (!iconHost) return
285+
const tpl = this._skeletonFor(variant)
286+
if (!tpl) return
287+
const sourceIcon = tpl.content.firstElementChild?.querySelector('[data-slot="icon"]')
288+
iconHost.innerHTML = sourceIcon ? sourceIcon.innerHTML : ""
289+
}
290+
281291
_uuid() {
282292
if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID()
283293
return Math.random().toString(36).slice(2) + Date.now().toString(36)

gem/test/ruby_ui/toast_test.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,52 @@ def test_flash_variant_helper
2727
assert_equal :warning, RubyUI::Toast.flash_variant("alert")
2828
assert_equal :default, RubyUI::Toast.flash_variant(:unknown)
2929
end
30+
31+
def test_action_renders_label_and_action_attr
32+
out = phlex { RubyUI.ToastAction(label: "Undo", on: "click->thing#undo") }
33+
assert_match(/Undo/, out)
34+
assert_match(/data-slot="action"/, out)
35+
assert_match(/data-action="click->thing#undo"/, out)
36+
end
37+
38+
def test_cancel_renders_label_and_dismiss_action
39+
out = phlex { RubyUI.ToastCancel(label: "Dismiss") }
40+
assert_match(/Dismiss/, out)
41+
assert_match(/data-slot="cancel"/, out)
42+
assert_match(/click-&gt;ruby-ui--toast#dismiss|click->ruby-ui--toast#dismiss/, out)
43+
end
44+
45+
def test_icon_renders_per_variant
46+
{
47+
success: /circle[^>]*r="10"/,
48+
info: /M12 16v-4/,
49+
warning: /m21\.73 18/,
50+
error: /6\.624a2 2 0 0 1 1\.414/,
51+
loading: /animate-spin/
52+
}.each do |variant, pattern|
53+
out = phlex { RubyUI.ToastIcon(variant: variant) }
54+
assert_match(pattern, out, "expected #{variant} icon to match #{pattern.inspect}")
55+
end
56+
end
57+
58+
def test_icon_default_variant_renders_nothing
59+
out = phlex { RubyUI.ToastIcon(variant: :default) }
60+
refute_match(/<svg/, out)
61+
end
62+
63+
def test_close_renders_dismiss_action
64+
out = phlex { RubyUI.ToastClose() }
65+
assert_match(/data-slot="close"/, out)
66+
assert_match(/click-&gt;ruby-ui--toast#dismiss|click->ruby-ui--toast#dismiss/, out)
67+
assert_match(/sr-only/, out)
68+
end
69+
70+
def test_title_and_description_slot_attrs
71+
title_out = phlex { RubyUI.ToastTitle { "Saved" } }
72+
desc_out = phlex { RubyUI.ToastDescription { "details" } }
73+
assert_match(/data-slot="title"/, title_out)
74+
assert_match(/Saved/, title_out)
75+
assert_match(/data-slot="description"/, desc_out)
76+
assert_match(/details/, desc_out)
77+
end
3078
end

0 commit comments

Comments
 (0)