-
-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #80 from bluzky/feature/collapsible
Implement collapsible component
- Loading branch information
Showing
2 changed files
with
252 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |