Skip to content

Commit

Permalink
Merge pull request #80 from bluzky/feature/collapsible
Browse files Browse the repository at this point in the history
Implement collapsible component
  • Loading branch information
bluzky authored Nov 6, 2024
2 parents 355e3df + 0fb8b85 commit 37395f1
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 0 deletions.
96 changes: 96 additions & 0 deletions lib/salad_ui/collapsible.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
defmodule SaladUI.Collapsible do
@moduledoc """
Implementation of Collapsible components.
## Examples:
<.collapsible id="collapsible-1" open let={builder}>
<.collapsible_trigger builder={builder}>
<.button variant="outline">Show content</.button>
</.collapsible_trigger>
<.collapsible_content>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>
</.collapsible_content>
</.collapsible>
"""
use SaladUI, :component

attr :id, :string,
required: true,
doc: "Id to identify collapsible component, collapsible_trigger uses this id to toggle content visibility"

attr :open, :boolean, default: false, doc: "Initial state of collapsible content"
attr :class, :string, default: nil
slot(:inner_block, required: true)

def collapsible(assigns) do
assigns =
assigns
|> assign(:builder, %{open: assigns[:open], id: assigns[:id]})
|> assign(:open, normalize_boolean(assigns[:open]))

~H"""
<div
phx-toggle-collapsible={toggle_collapsible(@builder)}
phx-mounted={@open && JS.exec("phx-toggle-collapsible", to: "##{@id}")}
class={classes(["inline-block relative", @class])}
id={@id}
>
<%= render_slot(@inner_block, @builder) %>
</div>
"""
end

@doc """
Render trigger for collapsible component.
"""
attr :builder, :map, required: true, doc: "Builder instance for collapsible component"
attr(:class, :string, default: nil)
slot(:inner_block, required: true)

def collapsible_trigger(assigns) do
~H"""
<div phx-click={JS.exec("phx-toggle-collapsible", to: "#" <> @builder.id)} class={@class}>
<%= render_slot(@inner_block) %>
</div>
"""
end

@doc """
Render content for collapsible component.
"""
attr(:class, :string, default: nil)
attr(:rest, :global)
slot(:inner_block, required: true)

def collapsible_content(assigns) do
~H"""
<div
class={
classes([
"collapsible-content hidden transition-all duration-200 ease-in-out",
@class
])
}
{@rest}
>
<%= render_slot(@inner_block) %>
</div>
"""
end

@doc """
Show collapsible content.
"""
def toggle_collapsible(js \\ %JS{}, %{id: id} = _builder) do
JS.toggle(js,
to: "##{id} .collapsible-content",
in: {"ease-out duration-200", "opacity-0", "opacity-100"},
out: {"ease-out", "opacity-100", "opacity-70"},
time: 200
)
end
end
156 changes: 156 additions & 0 deletions test/salad_ui/collapsible_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
defmodule SaladUI.CollapsibleTest do
use ExUnit.Case
use Phoenix.Component
import Phoenix.LiveViewTest

import SaladUI.Collapsible

describe "collapsible/1" do
test "renders collapsible component with required attributes" do
assigns = %{}

html =
rendered_to_string(~H"""
<.collapsible id="test-collapsible" open={false}>
Test Content
</.collapsible>
""")

assert html =~ ~s(id="test-collapsible")
assert html =~ ~s(phx-toggle-collapsible)
assert html =~ "Test Content"
end

test "applies custom class" do
assigns = %{}

html =
rendered_to_string(~H"""
<.collapsible id="test-collapsible" class="custom-class">
Test Content
</.collapsible>
""")

for class <- ~w(inline-block relative custom-class) do
assert html =~ class
end
end
end

describe "collapsible_trigger/1" do
test "renders trigger with click handler" do
assigns = %{}

html =
rendered_to_string(~H"""
<.collapsible id="test-collapsible">
<.collapsible_trigger builder={%{id: "test-collapsible", open: false}}>
Click me
</.collapsible_trigger>
</.collapsible>
""")

assert html =~ ~s(phx-click)
assert html =~ "test-collapsible"
assert html =~ "Click me"
end

test "applies custom class to trigger" do
assigns = %{}

html =
rendered_to_string(~H"""
<.collapsible_trigger
builder={%{id: "test-collapsible", open: false}}
class="custom-trigger-class"
>
Click me
</.collapsible_trigger>
""")

assert html =~ ~s(class="custom-trigger-class")
end
end

describe "collapsible_content/1" do
test "renders content with default classes" do
assigns = %{}

html =
rendered_to_string(~H"""
<.collapsible_content>
Hidden content
</.collapsible_content>
""")

for class <- ~w(collapsible-content hidden transition-all duration-200 ease-in-out) do
assert html =~ class
end
assert html =~ "Hidden content"
end

test "applies custom class to content" do
assigns = %{}

html =
rendered_to_string(~H"""
<.collapsible_content class="custom-content-class">
Hidden content
</.collapsible_content>
""")

assert html =~ "custom-content-class"
assert html =~ "Hidden content"
end

test "accepts and renders additional HTML attributes" do
assigns = %{}

html =
rendered_to_string(~H"""
<.collapsible_content data-test="test-content">
Content
</.collapsible_content>
""")

assert html =~ ~s(data-test="test-content")
end
end

describe "toggle_collapsible/2" do
test "returns JavaScript commands for toggling content" do
js = toggle_collapsible(%Phoenix.LiveView.JS{}, %{id: "test-collapsible"})

assert js.ops == [
["toggle", %{
to: "#test-collapsible .collapsible-content",
ins: [["ease-out", "duration-200"], ["opacity-0"], ["opacity-100"]],
outs: [["ease-out"], ["opacity-100"], ["opacity-70"]],
time: 200
}]
]
end
end

test "integration: renders complete collapsible with trigger and content" do
assigns = %{}

html =
rendered_to_string(~H"""
<.collapsible id="test-collapsible" :let={builder} open={false}>
<.collapsible_trigger builder={builder}>
<button>Toggle</button>
</.collapsible_trigger>
<.collapsible_content>
<p>Hidden Content</p>
</.collapsible_content>
</.collapsible>
""")

assert html =~ "Toggle"
assert html =~ "Hidden Content"
assert html =~ "collapsible-content"
assert html =~ ~s(phx-toggle-collapsible)
assert html =~ ~s(phx-click)
end
end

0 comments on commit 37395f1

Please sign in to comment.