Skip to content

Commit 9bc8fcb

Browse files
committed
Add Toast component
Extracted from Uchi's component library (www.uchiadmin.com).
1 parent 1a79853 commit 9bc8fcb

File tree

7 files changed

+313
-0
lines changed

7 files changed

+313
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
88
### Added
99

1010
* DateTime input field components.
11+
* Toast component.
1112

1213
### Changed
1314

app/components/flowbite/toast.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# frozen_string_literal: true
2+
3+
module Flowbite
4+
# Renders a toast notification element.
5+
#
6+
# See https://flowbite.com/docs/components/toast/
7+
#
8+
# @param class [Array<String>] Additional CSS classes for the toast container.
9+
# @param dismissible [Boolean] Whether the toast can be dismissed (default: true).
10+
# @param message [String] The message to display in the toast.
11+
# @param options [Hash] Additional HTML options for the toast container.
12+
# @param style [Symbol] The color style of the toast (:default, :success, :danger, :warning).
13+
class Toast < ViewComponent::Base
14+
class << self
15+
def classes
16+
["flex", "items-center", "w-full", "max-w-xs", "p-4", "text-body", "bg-neutral-primary-soft", "rounded-base", "shadow-xs", "border", "border-default"]
17+
end
18+
end
19+
20+
attr_reader :dismissible, :message, :options, :style
21+
22+
def initialize(message:, dismissible: true, style: :default, class: nil, **options)
23+
@message = message
24+
@style = style
25+
@dismissible = dismissible
26+
@class = Array.wrap(binding.local_variable_get(:class))
27+
@options = options
28+
end
29+
30+
def container_classes
31+
self.class.classes + @class
32+
end
33+
end
34+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div class="<%= container_classes.join(" ") %>">
2+
<svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
3+
<path d="<%= svg_path %>"/>
4+
</svg>
5+
</div>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# frozen_string_literal: true
2+
3+
module Flowbite
4+
class Toast
5+
# Renders an icon for a toast notification.
6+
#
7+
# @param style [Symbol] The color style of the icon (:default, :success, :danger, :warning).
8+
class Icon < ViewComponent::Base
9+
class << self
10+
def classes(style: :default)
11+
styles.fetch(style).fetch(:classes)
12+
end
13+
14+
def svg_path(style: :default)
15+
styles.fetch(style).fetch(:svg_path)
16+
end
17+
18+
# rubocop:disable Layout/LineLength
19+
def styles
20+
{
21+
default: {
22+
classes: ["inline-flex", "items-center", "justify-center", "shrink-0", "w-8", "h-8", "text-blue-500", "bg-blue-100", "rounded-lg", "dark:bg-blue-800", "dark:text-blue-200"],
23+
svg_path: "M15.147 15.085a7.159 7.159 0 0 1-6.189 3.307A6.713 6.713 0 0 1 3.1 15.444c-2.679-4.513.287-8.737.888-9.548A4.373 4.373 0 0 0 5 1.608c1.287.953 6.445 3.218 5.537 10.5 1.5-1.122 2.706-3.01 2.853-6.14 1.433 1.049 3.993 5.395 1.757 9.117Z"
24+
},
25+
success: {
26+
classes: ["inline-flex", "items-center", "justify-center", "shrink-0", "w-8", "h-8", "text-green-500", "bg-green-100", "rounded-lg", "dark:bg-green-800", "dark:text-green-200"],
27+
svg_path: "M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"
28+
},
29+
danger: {
30+
classes: ["inline-flex", "items-center", "justify-center", "shrink-0", "w-8", "h-8", "text-red-500", "bg-red-100", "rounded-lg", "dark:bg-red-800", "dark:text-red-200"],
31+
svg_path: "M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"
32+
},
33+
warning: {
34+
classes: ["inline-flex", "items-center", "justify-center", "shrink-0", "w-8", "h-8", "text-orange-500", "bg-orange-100", "rounded-lg", "dark:bg-orange-700", "dark:text-orange-200"],
35+
svg_path: "M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"
36+
}
37+
}.freeze
38+
end
39+
# rubocop:enable Layout/LineLength
40+
end
41+
42+
attr_reader :style
43+
44+
def initialize(style: :default)
45+
@style = style
46+
end
47+
48+
def container_classes
49+
self.class.classes(style: style)
50+
end
51+
52+
def svg_path
53+
self.class.svg_path(style: style)
54+
end
55+
end
56+
end
57+
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<div
2+
class="<%= container_classes.join(" ") %>"
3+
role="alert"
4+
<%= options.map { |k, v| "#{k}=\"#{v}\"" }.join(" ").html_safe %>
5+
>
6+
<%= render Flowbite::Toast::Icon.new(style: style) %>
7+
8+
<div class="ms-3 text-sm font-normal"><%= message %></div>
9+
10+
<% if dismissible %>
11+
<%# Styles from https://flowbite.com/docs/components/toast/#default-toast %>
12+
<button
13+
type="button"
14+
class="
15+
ms-auto flex items-center justify-center text-body
16+
hover:text-heading bg-transparent box-border border
17+
border-transparent hover:bg-neutral-secondary-medium focus:ring-4
18+
focus:ring-neutral-tertiary font-medium leading-5 rounded text-sm
19+
h-8 w-8 focus:outline-none
20+
"
21+
aria-label="Close"
22+
>
23+
<svg
24+
class="w-3 h-3"
25+
aria-hidden="true"
26+
xmlns="http://www.w3.org/2000/svg"
27+
fill="none"
28+
viewBox="0 0 14 14"
29+
>
30+
<path
31+
stroke="currentColor"
32+
stroke-linecap="round"
33+
stroke-linejoin="round"
34+
stroke-width="2"
35+
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
36+
/>
37+
</svg>
38+
</button>
39+
<% end %>
40+
</div>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
class ToastPreview < Lookbook::Preview
4+
# @!group Styles
5+
#
6+
# Use these toast notification styles to show feedback messages to your users.
7+
# Toast notifications appear with different colors and icons based on the style.
8+
#
9+
# @display classes flex flex-col space-y-4
10+
11+
def default
12+
render(Flowbite::Toast.new(message: "Improvement archived successfully."))
13+
end
14+
15+
def success
16+
render(Flowbite::Toast.new(message: "Item moved successfully.", style: :success))
17+
end
18+
19+
def danger
20+
render(Flowbite::Toast.new(message: "Item has been deleted.", style: :danger))
21+
end
22+
23+
def warning
24+
render(Flowbite::Toast.new(message: "Improve password difficulty.", style: :warning))
25+
end
26+
27+
# @!endgroup
28+
29+
# @!group Dismissible
30+
#
31+
# Control whether the toast can be dismissed with a close button.
32+
#
33+
# @display classes flex flex-col space-y-4
34+
35+
def with_dismiss_button
36+
render(Flowbite::Toast.new(message: "This toast can be dismissed.", dismissible: true))
37+
end
38+
39+
def without_dismiss_button
40+
render(Flowbite::Toast.new(message: "This toast cannot be dismissed.", dismissible: false))
41+
end
42+
43+
# @!endgroup
44+
45+
# @!group Custom styling
46+
#
47+
# Add custom classes to the toast container.
48+
#
49+
# @display classes flex flex-col space-y-4
50+
51+
def with_custom_classes
52+
render(Flowbite::Toast.new(message: "Custom styled toast.", class: ["bg-blue-50", "border-blue-200"]))
53+
end
54+
55+
# @!endgroup
56+
end
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
require "test_helper"
2+
3+
class Flowbite::ToastTest < Minitest::Test
4+
include ViewComponent::TestHelpers
5+
6+
def test_render_component
7+
render_inline(Flowbite::Toast.new(message: "Test message"))
8+
9+
assert_component_rendered
10+
assert_selector("div[role='alert']", text: "Test message")
11+
end
12+
13+
def test_renders_message
14+
render_inline(Flowbite::Toast.new(message: "Hello, World!"))
15+
16+
assert_selector("div", text: "Hello, World!")
17+
end
18+
19+
def test_renders_with_default_classes
20+
render_inline(Flowbite::Toast.new(message: "Test"))
21+
22+
assert_selector("div.flex.items-center.w-full.max-w-xs.p-4")
23+
end
24+
25+
def test_renders_with_additional_classes
26+
render_inline(Flowbite::Toast.new(message: "Test", class: ["custom-class", "another-class"]))
27+
28+
assert_selector("div.custom-class.another-class")
29+
end
30+
31+
def test_renders_dismissible_button_by_default
32+
render_inline(Flowbite::Toast.new(message: "Test"))
33+
34+
assert_selector("button[type='button'][aria-label='Close']")
35+
end
36+
37+
def test_renders_without_dismissible_button
38+
render_inline(Flowbite::Toast.new(message: "Test", dismissible: false))
39+
40+
assert_no_selector("button[type='button'][aria-label='Close']")
41+
end
42+
43+
def test_renders_with_custom_html_options
44+
render_inline(Flowbite::Toast.new(message: "Test", id: "my-toast", "data-controller": "toast"))
45+
46+
assert_selector("div[id='my-toast'][data-controller='toast']")
47+
end
48+
49+
def test_renders_default_style_icon
50+
render_inline(Flowbite::Toast.new(message: "Test", style: :default))
51+
52+
assert_selector("div.text-blue-500.bg-blue-100")
53+
end
54+
55+
def test_renders_success_style_icon
56+
render_inline(Flowbite::Toast.new(message: "Test", style: :success))
57+
58+
assert_selector("div.text-green-500.bg-green-100")
59+
end
60+
61+
def test_renders_danger_style_icon
62+
render_inline(Flowbite::Toast.new(message: "Test", style: :danger))
63+
64+
assert_selector("div.text-red-500.bg-red-100")
65+
end
66+
67+
def test_renders_warning_style_icon
68+
render_inline(Flowbite::Toast.new(message: "Test", style: :warning))
69+
70+
assert_selector("div.text-orange-500.bg-orange-100")
71+
end
72+
end
73+
74+
class Flowbite::Toast::IconTest < Minitest::Test
75+
include ViewComponent::TestHelpers
76+
77+
def test_render_component
78+
render_inline(Flowbite::Toast::Icon.new)
79+
80+
assert_component_rendered
81+
assert_selector("svg")
82+
end
83+
84+
def test_renders_default_style
85+
render_inline(Flowbite::Toast::Icon.new(style: :default))
86+
87+
assert_selector("div.text-blue-500.bg-blue-100")
88+
end
89+
90+
def test_renders_success_style
91+
render_inline(Flowbite::Toast::Icon.new(style: :success))
92+
93+
assert_selector("div.text-green-500.bg-green-100")
94+
end
95+
96+
def test_renders_danger_style
97+
render_inline(Flowbite::Toast::Icon.new(style: :danger))
98+
99+
assert_selector("div.text-red-500.bg-red-100")
100+
end
101+
102+
def test_renders_warning_style
103+
render_inline(Flowbite::Toast::Icon.new(style: :warning))
104+
105+
assert_selector("div.text-orange-500.bg-orange-100")
106+
end
107+
108+
def test_renders_svg_with_proper_attributes
109+
render_inline(Flowbite::Toast::Icon.new)
110+
111+
assert_selector("svg[aria-hidden='true']")
112+
assert_selector("svg[fill='currentColor']")
113+
end
114+
115+
def test_renders_icon_with_proper_classes
116+
render_inline(Flowbite::Toast::Icon.new)
117+
118+
assert_selector("div.inline-flex.items-center.justify-center.shrink-0.w-8.h-8")
119+
end
120+
end

0 commit comments

Comments
 (0)