Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions app/components/flowbite/dropdown.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

module Flowbite
# Renders a dropdown menu triggered by a button.
#
# The dropdown menu itself is toggled using Flowbite's JavaScript, via the
# +data-dropdown-toggle+ attribute. Make sure Flowbite's JavaScript is
# loaded on the page for the toggle behavior to work.
Comment on lines +6 to +8
#
# @example Usage
# <%= render(Flowbite::Dropdown.new(id: "user-menu", label: "Dropdown button")) do |dropdown| %>
# <% dropdown.with_item do %>
# <%= render(Flowbite::Dropdown::Item.new(href: "/dashboard")) { "Dashboard" } %>
# <% end %>
# <% dropdown.with_item do %>
# <%= render(Flowbite::Dropdown::Item.new(href: "/settings")) { "Settings" } %>
# <% end %>
# <% end %>
#
# @viewcomponent_slot [Flowbite::Dropdown::Item] items The items of the
# dropdown menu.
#
# @see https://flowbite.com/docs/components/dropdowns/
# @lookbook_embed DropdownPreview
class Dropdown < ViewComponent::Base
renders_many :items

class << self
def menu_classes
["z-10", "hidden", "bg-neutral-primary-soft", "divide-y", "divide-default", "rounded-base", "shadow-lg", "w-44"]
end
end

attr_reader :button_options, :id, :label

# @param button_options [Hash] Additional options passed to the trigger
# {Flowbite::Button}, such as +style+ or +size+.
# @param class [Array<String>] Additional CSS classes for the dropdown menu.
# @param id [String] A unique id for the dropdown, used to associate the
# trigger button with the menu.
# @param label [String] The text shown on the trigger button.
def initialize(id:, label:, class: nil, **button_options)
@id = id
@label = label
@class = Array.wrap(binding.local_variable_get(:class))
@button_options = button_options
end

private

def button_id
"#{id}-button"
end

def menu_classes
self.class.menu_classes + @class
end
end
end
32 changes: 32 additions & 0 deletions app/components/flowbite/dropdown/chevron_icon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

module Flowbite
class Dropdown
# Renders the chevron-down icon shown on a dropdown's trigger button.
#
# This is automatically used by {Flowbite::Dropdown}, but can be used
# standalone if needed.
#
# @example Standalone usage
# <%= render Flowbite::Dropdown::ChevronIcon.new %>
class ChevronIcon < ViewComponent::Base
def call
tag.svg(
class: "w-2.5 h-2.5 ms-3",
"aria-hidden": "true",
xmlns: "http://www.w3.org/2000/svg",
fill: "none",
viewBox: "0 0 10 6"
) do
tag.path(
stroke: "currentColor",
"stroke-linecap": "round",
"stroke-linejoin": "round",
"stroke-width": "2",
d: "m1 1 4 4 4-4"
)
end
end
end
end
end
17 changes: 17 additions & 0 deletions app/components/flowbite/dropdown/dropdown.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div class="relative inline-block">
<%= render(Flowbite::Button.new(id: button_id, "data-dropdown-toggle": id, **button_options)) do %>
<span class="inline-flex items-center">
<%= label %>

<%= render(Flowbite::Dropdown::ChevronIcon.new) %>
</span>
<% end %>

<div id="<%= id %>" class="<%= menu_classes.join(" ") %>">
<ul class="py-2 text-sm text-body" aria-labelledby="<%= button_id %>">
<% items.each do |item| %>
<%= item %>
<% end %>
</ul>
</div>
</div>
61 changes: 61 additions & 0 deletions app/components/flowbite/dropdown/item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

module Flowbite
class Dropdown
# Renders an item in a dropdown menu.
#
# If +href+ is given, the item renders as a link. Otherwise, it renders as
# a button, which is useful for items that trigger a JavaScript action or
# submit a form.
#
# @param class [Array<String>] Additional CSS classes for the item element.
# @param href [String] The URL for the item. If omitted, the item renders
# as a +<button>+ instead of a link.
# @param options [Hash] Additional HTML attributes for the item element.
#
# @example Link item
# <%= render Flowbite::Dropdown::Item.new(href: "/dashboard") { "Dashboard" } %>
#
# @example Button item
# <%= render Flowbite::Dropdown::Item.new(type: "submit") { "Sign out" } %>
class Item < ViewComponent::Base
class << self
def classes
["block", "w-full", "px-4", "py-2", "text-left", "text-sm", "text-body", "hover:bg-neutral-tertiary-medium", "hover:text-heading"]
end
end

attr_reader :href, :options

def initialize(href: nil, class: nil, **options)
@class = Array.wrap(binding.local_variable_get(:class))
@href = href
@options = options
end

def call
content_tag(:li) { render_link }
end

private

def classes
self.class.classes + @class
end

def link?
href.present?
end

def render_link
link_options = {class: classes}.merge(options)

if link?
content_tag(:a, content, href: href, **link_options)
else
content_tag(:button, content, type: "button", **link_options)
end
end
Comment on lines +36 to +58
end
end
end
3 changes: 3 additions & 0 deletions demo/.yardoc/checksums
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ app/components/flowbite/button.rb 6ae7681d3b842d73aa99cddfa5a9b107ede7fea4
app/components/flowbite/styles.rb 929c42e428ba5a8e16efacaae0f35380e2f5f95c
app/components/flowbite/sidebar.rb 85033b602a098f3334b9b3e180239ef20a1b6f90
app/components/flowbite/spinner.rb acb87459a5697d8fb3b9a9b0f7dc6b8e893991c0
app/components/flowbite/dropdown.rb 2be72c864440c7213e9ec9a8e620bf4fb2b1af30
app/components/flowbite/badge/dot.rb 0e899c005a5e95a6b15b5a60ce9c25706c8521a1
app/components/flowbite/input/url.rb f1046824f9b06c8df8e0f567979321b82baac6fa
app/components/flowbite/badge/pill.rb cf713c5935e9300648f5501c90dced0aca488472
Expand All @@ -25,6 +26,7 @@ app/components/flowbite/input_field.rb 5f82de53dbd8e331f7118c1e762441eb37f76783
app/components/flowbite/input/number.rb a33580788ad91308b85955fdb44d37883329dd4e
app/components/flowbite/input/select.rb 9f1a6406efdda2e29d479117a35c2a924bd888c2
app/components/flowbite/sidebar/item.rb 8dc762357988fdf6e9308de129bd7c89b6700472
app/components/flowbite/dropdown/item.rb f4e5949970c085bf76ae35fa542ad3fad8a38abf
app/components/flowbite/button/outline.rb 2829cf352a03c00dd99a56a05181c4e1a6794d18
app/components/flowbite/input/checkbox.rb 500f109206a47997bf2bc0732399297a92005dc0
app/components/flowbite/input/password.rb 39a4c2bb2684a0a310175307bd1bdfd9c99c6cd1
Expand All @@ -46,6 +48,7 @@ app/components/flowbite/input_field/checkbox.rb 6fff8f304ccb52fca3520719ac6487c6
app/components/flowbite/input_field/password.rb 37e592f55f258cc4b81859d48763b9c0eff12f61
app/components/flowbite/input_field/textarea.rb 52626e199b25b723b4d5355028d6bc3d7a167cd1
app/components/flowbite/breadcrumb/item/first.rb 0d755f7f7fbf6980b4667b8b723d4dce94ca2224
app/components/flowbite/dropdown/chevron_icon.rb 536e3604387b30a375571722fef3b03b1b13f5c5
app/components/flowbite/input_field/date_time.rb 0a17d49d70f3ba820fee0f8930115540f12d72e2
app/components/flowbite/input/validation_error.rb b6221a7a542136dc05d0053bda61061c7d4829d8
app/components/flowbite/breadcrumb/item/current.rb 73551aac4b45f3045e14ccdc74187e419d099176
Expand Down
Binary file modified demo/.yardoc/object_types
Binary file not shown.
Binary file modified demo/.yardoc/objects/root.dat
Binary file not shown.
49 changes: 49 additions & 0 deletions demo/test/components/previews/dropdown_preview.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

class DropdownPreview < Lookbook::Preview
def example
render(Flowbite::Dropdown.new(id: "example-dropdown", label: "Dropdown button")) do |dropdown|
dropdown.with_item { render(Flowbite::Dropdown::Item.new(href: "#")) { "Dashboard" } }
dropdown.with_item { render(Flowbite::Dropdown::Item.new(href: "#")) { "Settings" } }
dropdown.with_item { render(Flowbite::Dropdown::Item.new(href: "#")) { "Earnings" } }
dropdown.with_item { render(Flowbite::Dropdown::Item.new(href: "#")) { "Sign out" } }
end
end

# @!group Button styles
#
# The trigger button accepts the same style options as {Flowbite::Button}.
#
# @display classes flex items-center space-x-4

def default
render(Flowbite::Dropdown.new(id: "default-dropdown", label: "Default")) do |dropdown|
dropdown.with_item { render(Flowbite::Dropdown::Item.new(href: "#")) { "Dashboard" } }
dropdown.with_item { render(Flowbite::Dropdown::Item.new(href: "#")) { "Settings" } }
end
end

def secondary
render(Flowbite::Dropdown.new(id: "secondary-dropdown", label: "Secondary", style: :secondary)) do |dropdown|
dropdown.with_item { render(Flowbite::Dropdown::Item.new(href: "#")) { "Dashboard" } }
dropdown.with_item { render(Flowbite::Dropdown::Item.new(href: "#")) { "Settings" } }
end
end

# @!endgroup

# @!group Button items
#
# Omit +href+ on an item to render it as a button instead of a link. This is
# useful for items that submit a form or trigger a JavaScript action.
#
# @display classes flex items-center space-x-4

def with_button_items
render(Flowbite::Dropdown.new(id: "button-items-dropdown", label: "Dropdown button")) do |dropdown|
dropdown.with_item { render(Flowbite::Dropdown::Item.new) { "Sign out" } }
end
end

# @!endgroup
end
94 changes: 94 additions & 0 deletions test/components/flowbite/dropdown_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
require "test_helper"

class Flowbite::DropdownTest < Minitest::Test
include ViewComponent::TestHelpers

def test_render_component
render_inline(Flowbite::Dropdown.new(id: "user-menu", label: "Dropdown button"))

assert_component_rendered
assert_selector("button", text: "Dropdown button")
assert_selector("div#user-menu")
end

def test_renders_button_with_dropdown_toggle_attribute
render_inline(Flowbite::Dropdown.new(id: "user-menu", label: "Dropdown button"))

assert_selector("button#user-menu-button[data-dropdown-toggle='user-menu']")
end

def test_renders_menu_labelled_by_button
render_inline(Flowbite::Dropdown.new(id: "user-menu", label: "Dropdown button"))

assert_selector("ul[aria-labelledby='user-menu-button']")
end

def test_renders_menu_hidden_by_default
render_inline(Flowbite::Dropdown.new(id: "user-menu", label: "Dropdown button"))

assert_selector("div#user-menu.hidden")
end

def test_passes_button_options_to_trigger_button
render_inline(Flowbite::Dropdown.new(id: "user-menu", label: "Dropdown button", style: :secondary))

assert_selector("button.bg-neutral-secondary-medium")
end

def test_renders_with_additional_menu_classes
render_inline(Flowbite::Dropdown.new(id: "user-menu", label: "Dropdown button", class: ["custom-class"]))

assert_selector("div#user-menu.custom-class")
end

def test_renders_items
render_inline(Flowbite::Dropdown.new(id: "user-menu", label: "Dropdown button")) do |dropdown|
dropdown.with_item { "<li><a href='/dashboard'>Dashboard</a></li>".html_safe }
dropdown.with_item { "<li><a href='/settings'>Settings</a></li>".html_safe }
end

assert_selector("li a[href='/dashboard']", text: "Dashboard")
assert_selector("li a[href='/settings']", text: "Settings")
end
end

class Flowbite::Dropdown::ItemTest < Minitest::Test
include ViewComponent::TestHelpers

def test_render_component
render_inline(Flowbite::Dropdown::Item.new(href: "/dashboard")) { "Dashboard" }

assert_component_rendered
assert_selector("li a[href='/dashboard']", text: "Dashboard")
end

def test_renders_as_button_without_href
render_inline(Flowbite::Dropdown::Item.new) { "Sign out" }

assert_selector("li button[type='button']", text: "Sign out")
assert_no_selector("a")
end

def test_renders_with_additional_classes
render_inline(Flowbite::Dropdown::Item.new(href: "/dashboard", class: ["custom-class"])) { "Dashboard" }

assert_selector("a.custom-class")
end

def test_renders_with_custom_html_options
render_inline(Flowbite::Dropdown::Item.new(href: "/dashboard", id: "my-item")) { "Dashboard" }

assert_selector("a#my-item")
end
end

class Flowbite::Dropdown::ChevronIconTest < Minitest::Test
include ViewComponent::TestHelpers

def test_render_component
render_inline(Flowbite::Dropdown::ChevronIcon.new)

assert_component_rendered
assert_selector("svg[aria-hidden='true']")
end
end