Skip to content

Commit

Permalink
stepper wip
Browse files Browse the repository at this point in the history
  • Loading branch information
nhobes committed Oct 29, 2024
1 parent 8c5ae0d commit ec5c69f
Show file tree
Hide file tree
Showing 3 changed files with 287 additions and 12 deletions.
30 changes: 18 additions & 12 deletions lib/petal_components/stepper.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
defmodule PetalComponents.Stepper do
use Phoenix.Component
import Phoenix.HTML
import PetalComponents.Icon

attr :steps, :list, required: true
Expand All @@ -10,15 +9,19 @@ defmodule PetalComponents.Stepper do

def stepper(assigns) do
~H"""
<div class={[
"pc-stepper",
"pc-stepper--#{@orientation}",
"pc-stepper--#{@size}",
@class
]}>
<div
class={[
"pc-stepper",
"pc-stepper--#{@orientation}",
"pc-stepper--#{@size}",
@class
]}
role="group"
aria-label="Progress steps"
>
<div class="pc-stepper__container">
<%= for {step, index} <- Enum.with_index(@steps) do %>
<div class="pc-stepper__item">
<div class="pc-stepper__item" role="listitem">
<div class="pc-stepper__item-content">
<div
class={[
Expand All @@ -28,8 +31,11 @@ defmodule PetalComponents.Stepper do
]}
id={"step-#{index}"}
phx-click={step[:on_click]}
role="button"
aria-current={step.active? && "step"}
aria-label={"Step #{index + 1}: #{step.name}#{if step.complete?, do: " (completed)"}"}
>
<div class="pc-stepper__indicator">
<div class="pc-stepper__indicator" aria-hidden="true">
<%= if step.complete? do %>
<.icon name="hero-check-solid" class="pc-stepper__check" />
<% else %>
Expand All @@ -39,19 +45,19 @@ defmodule PetalComponents.Stepper do
<% end %>
</div>
<div class="pc-stepper__content">
<h3 class="pc-stepper__title">
<h3 class="pc-stepper__title" id={"step-title-#{index}"}>
<%= step.name %>
</h3>
<%= if Map.get(step, :description) do %>
<p class="pc-stepper__description">
<p class="pc-stepper__description" id={"step-description-#{index}"}>
<%= step.description %>
</p>
<% end %>
</div>
</div>
</div>
<%= if index < length(@steps) - 1 do %>
<div class="pc-stepper__connector-wrapper">
<div class="pc-stepper__connector-wrapper" aria-hidden="true">
<div class={[
"pc-stepper__connector",
step.complete? && Enum.at(@steps, index + 1).complete? &&
Expand Down
29 changes: 29 additions & 0 deletions lib/petal_components_web/a11y_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule PetalComponentsWeb.A11yLive do
"""
use Phoenix.LiveView, global_prefixes: ~w(x-)
use PetalComponents
alias Phoenix.LiveView.JS

def mount(_params, _session, socket) do
{:ok,
Expand Down Expand Up @@ -184,6 +185,34 @@ defmodule PetalComponentsWeb.A11yLive do
<.spinner show={true} />
<.stepper
steps={[
%{
name: "Account Details",
description: "Basic information",
complete?: true,
active?: true,
on_click: JS.push("navigate", value: %{target_index: 0})
},
%{
name: "Preferences",
description: "Set preferences",
complete?: true,
active?: false,
on_click: JS.push("navigate", value: %{target_index: 1})
},
%{
name: "Confirmation",
description: "Review and confirm",
complete?: false,
active?: false,
on_click: JS.push("navigate", value: %{target_index: 2})
}
]}
orientation="horizontal"
size="sm"
/>
<.vertical_menu
menu_items={@main_menu_items}
current_page={@current_page}
Expand Down
240 changes: 240 additions & 0 deletions test/petal/stepper_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
defmodule PetalComponents.StepperTest do
use ComponentCase
import PetalComponents.Stepper

@sample_steps [
%{
name: "Step 1",
description: "First step",
complete?: true,
active?: false,
on_click: "step-1"
},
%{
name: "Step 2",
description: "Second step",
complete?: true,
active?: false,
on_click: "step-2"
},
%{
name: "Step 3",
description: "Third step",
complete?: false,
active?: true,
on_click: "step-3"
}
]

describe "basic rendering" do
test "renders with proper accessibility attributes" do
assigns = %{sample_steps: @sample_steps}

html =
rendered_to_string(~H"""
<nav aria-label="Progress">
<.stepper steps={@sample_steps} class="my-8" />
</nav>
""")

assert html =~ "aria-label=\"Progress steps\""
assert html =~ "role=\"group\""
assert html =~ "aria-current=\"step\""
end

test "renders correct number of steps" do
assigns = %{sample_steps: @sample_steps}

html =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} />
""")

indicators = html |> String.split("pc-stepper__indicator") |> length() |> Kernel.-(1)
connectors = html |> String.split("pc-stepper__connector ") |> length() |> Kernel.-(1)

assert indicators == 3
assert connectors == 2
end

test "renders check icons for completed steps" do
assigns = %{sample_steps: @sample_steps}

html =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} />
""")

check_icons = html |> String.split("hero-check-solid") |> length() |> Kernel.-(1)
completed_steps = @sample_steps |> Enum.count(& &1.complete?)

assert check_icons == completed_steps
end

test "renders step descriptions when provided" do
assigns = %{sample_steps: @sample_steps}

html =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} />
""")

description_count =
html |> String.split("pc-stepper__description") |> length() |> Kernel.-(1)

descriptions_provided = @sample_steps |> Enum.count(& &1.description)

assert description_count == descriptions_provided
end
end

describe "props" do
test "correctly applies orientation variants" do
assigns = %{sample_steps: @sample_steps}

horizontal_html =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} orientation="horizontal" />
""")

vertical_html =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} orientation="vertical" />
""")

assert horizontal_html =~ "pc-stepper--horizontal"
assert vertical_html =~ "pc-stepper--vertical"
end

test "correctly applies size variants" do
assigns = %{sample_steps: @sample_steps}

html_sm =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} size="sm" />
""")

html_md =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} size="md" />
""")

html_lg =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} size="lg" />
""")

assert html_sm =~ "pc-stepper--sm"
assert html_md =~ "pc-stepper--md"
assert html_lg =~ "pc-stepper--lg"
end

test "applies custom classes" do
assigns = %{sample_steps: @sample_steps}

html =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} class="custom-class" />
""")

assert html =~ "custom-class"
end
end

describe "step states" do
test "applies active class to current step" do
assigns = %{sample_steps: @sample_steps}

html =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} />
""")

active_steps = html |> String.split("pc-stepper__node--active") |> length() |> Kernel.-(1)
expected_active = @sample_steps |> Enum.count(& &1.active?)

assert active_steps == expected_active
end

test "applies complete class to finished steps" do
assigns = %{sample_steps: @sample_steps}

html =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} />
""")

complete_nodes =
html |> String.split("pc-stepper__node--complete") |> length() |> Kernel.-(1)

expected_complete = @sample_steps |> Enum.count(& &1.complete?)

assert complete_nodes == expected_complete
end

test "correctly styles connectors between complete steps" do
assigns = %{sample_steps: @sample_steps}

html =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} />
""")

complete_connectors =
html |> String.split("pc-stepper__connector--complete") |> length() |> Kernel.-(1)

# Count how many adjacent complete steps we have
adjacent_complete =
@sample_steps
|> Enum.chunk_every(2, 1, :discard)
|> Enum.count(fn [a, b] -> a.complete? && b.complete? end)

assert complete_connectors == adjacent_complete
end
end

describe "edge cases" do
test "handles single step" do
assigns = %{sample_steps: [@sample_steps |> List.first()]}

html =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} />
""")

connectors = html |> String.split("pc-stepper__connector") |> length() |> Kernel.-(1)
assert connectors == 0, "Single step should not render any connectors"
end

test "handles steps without descriptions" do
steps_without_descriptions =
Enum.map(@sample_steps, fn step -> Map.delete(step, :description) end)

assigns = %{sample_steps: steps_without_descriptions}

html =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} />
""")

descriptions = html |> String.split("pc-stepper__description") |> length() |> Kernel.-(1)

assert descriptions == 0,
"Steps without descriptions should not render description elements"
end

test "handles steps without on_click" do
steps_without_onclick =
Enum.map(@sample_steps, fn step -> Map.delete(step, :on_click) end)

assigns = %{sample_steps: steps_without_onclick}

html =
rendered_to_string(~H"""
<.stepper steps={@sample_steps} />
""")

refute html =~ "phx-click", "Steps without on_click should not render phx-click attribute"
end
end
end

0 comments on commit ec5c69f

Please sign in to comment.