Skip to content
Draft
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
98 changes: 98 additions & 0 deletions lib/ash_ui/rendering/live_ui_adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,104 @@ defmodule AshUI.Rendering.LiveUIAdapter do
"""
end

defp generate_heex(%{"type" => "top_strip"} = iur, opts) do
props = iur["props"] || %{}
title = escaped_text_prop(props, ["title", "label"], "")
brand = escaped_text_prop(props, "brand", "")
context = escaped_text_prop(props, "context", "")

"""
<header class="#{css_classes(["ash-top-strip", prop_class(iur)])}" data-live-ui-shell-position="top"#{style_attr(prop_style(iur))}>
#{if brand != "", do: "<span class=\"ash-top-strip-brand\">#{brand}</span>", else: ""}
#{if title != "", do: "<h2 class=\"ash-top-strip-title\">#{title}</h2>", else: ""}
#{if context != "", do: "<span class=\"ash-top-strip-context\">#{context}</span>", else: ""}
#{generate_children(iur["children"], opts)}
</header>
"""
end

defp generate_heex(%{"type" => "sidebar_section"} = iur, opts) do
props = iur["props"] || %{}
label = escaped_text_prop(props, "label", "")
action_label = escaped_text_prop(props, ["action_label", "action_glyph"], "+")
action_intent = truthy_prop(props, "action_intent", false)

action_html =
if action_intent do
~s(<button type="button" class="ash-sidebar-section-action">#{action_label}</button>)
else
""
end

"""
<section class="#{css_classes(["ash-sidebar-section", prop_class(iur)])}"#{style_attr(prop_style(iur))}>
<div class="ash-sidebar-section-header">
<h3 class="ash-sidebar-section-label">#{label}</h3>
#{action_html}
</div>
#{generate_children(iur["children"], opts)}
</section>
"""
end

defp generate_heex(%{"type" => "sidebar_item"} = iur, opts) do
props = iur["props"] || %{}
label = escaped_text_prop(props, "label", "")
selected? = truthy_prop(props, "selected?", truthy_prop(props, "selected", false))
aria_current = if selected?, do: ~s( aria-current="page"), else: ""

"""
<li class="#{css_classes(["ash-sidebar-item", selected? && "ash-sidebar-item--selected", prop_class(iur)])}"#{style_attr(prop_style(iur))}>
<button type="button" class="ash-sidebar-item-button"#{aria_current}>
#{label}
#{generate_children(iur["children"], opts)}
</button>
</li>
"""
end

defp generate_heex(%{"type" => "tabs"} = iur, _opts) do
props = iur["props"] || %{}
items = prop(props, "items", []) |> List.wrap()
active_item_id = text_prop(props, "active_item_id")

tabs_html =
Enum.map_join(items, fn item ->
item = normalize_item(item)
item_id = text_prop(item, ["id", "item_id"], "")
item_label = escaped_text_prop(item, "label", "")
selected? = active_item_id && to_string(item_id) == to_string(active_item_id)
aria_selected = if selected?, do: "true", else: "false"
tab_index = if selected?, do: "0", else: "-1"

~s(<button type="button" role="tab" aria-selected="#{aria_selected}" tabindex="#{tab_index}" data-item-id="#{html_attr(item_id)}">#{item_label}</button>)
end)

"""
<div class="#{css_classes(["ash-tabs", prop_class(iur)])}" data-live-ui-widget="tabs"#{style_attr(prop_style(iur))}>
<div role="tablist">
#{tabs_html}
</div>
</div>
"""
end

defp generate_heex(%{"type" => "tree_view"} = iur, _opts) do
props = iur["props"] || %{}
nodes = prop(props, "nodes", []) |> List.wrap()
selection_mode = escaped_text_prop(props, "selection_mode", "single")

nodes_html = render_tree_nodes(nodes)

"""
<section class="#{css_classes(["ash-tree-view", prop_class(iur)])}" data-live-ui-widget="tree-view" data-live-ui-selection-mode="#{selection_mode}"#{style_attr(prop_style(iur))}>
<ul>
#{nodes_html}
</ul>
</section>
"""
end

defp generate_heex(%{"type" => "slide_over_panel"} = iur, opts) do
props = iur["props"] || %{}
label = escaped_text_prop(props, ["label", "title"], "Panel")
Expand Down
167 changes: 167 additions & 0 deletions test/ash_ui/rendering/live_ui_adapter_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1257,4 +1257,171 @@ defmodule AshUI.Rendering.LiveUIAdapterTest do
assert heex =~ ~s(data-selected="true")
end
end

describe "shell-primitive generate_heex clauses (gap-1)" do
test "top_strip renders header with ash-top-strip class and brand/context spans" do
iur = %{
"type" => "top_strip",
"id" => "top-strip-1",
"props" => %{"title" => "Dashboard", "brand" => "Ariston", "context" => "dev"},
"children" => [],
"metadata" => %{}
}

{:ok, heex} = LiveUIAdapter.render(iur, force_fallback: true)

assert heex =~ "<header"
assert heex =~ "ash-top-strip"
assert heex =~ "ash-top-strip-brand"
assert heex =~ "Ariston"
assert heex =~ "ash-top-strip-context"
assert heex =~ "dev"
end

test "top_strip renders children inside the header" do
iur = %{
"type" => "top_strip",
"id" => "top-strip-2",
"props" => %{"brand" => "Ariston"},
"children" => [
%{
"type" => "text",
"id" => "action-1",
"props" => %{"content" => "Actions"},
"children" => [],
"metadata" => %{}
}
],
"metadata" => %{}
}

{:ok, heex} = LiveUIAdapter.render(iur, force_fallback: true)

assert heex =~ "ash-top-strip"
assert heex =~ "Actions"
end

test "sidebar_section renders section with heading label and children" do
iur = %{
"type" => "sidebar_section",
"id" => "sidebar-section-1",
"props" => %{"label" => "Navigation"},
"children" => [
%{
"type" => "text",
"id" => "nav-item-1",
"props" => %{"content" => "Home"},
"children" => [],
"metadata" => %{}
}
],
"metadata" => %{}
}

{:ok, heex} = LiveUIAdapter.render(iur, force_fallback: true)

assert heex =~ "<section"
assert heex =~ "ash-sidebar-section"
assert heex =~ "<h3"
assert heex =~ "Navigation"
assert heex =~ "Home"
end

test "sidebar_section renders action button when action_intent is set" do
iur = %{
"type" => "sidebar_section",
"id" => "sidebar-section-2",
"props" => %{"label" => "Workspaces", "action_intent" => true, "action_label" => "Add"},
"children" => [],
"metadata" => %{}
}

{:ok, heex} = LiveUIAdapter.render(iur, force_fallback: true)

assert heex =~ "ash-sidebar-section-action"
assert heex =~ "Add"
end

test "sidebar_item renders li with button and label" do
iur = %{
"type" => "sidebar_item",
"id" => "sidebar-item-1",
"props" => %{"label" => "Proposals"},
"children" => [],
"metadata" => %{}
}

{:ok, heex} = LiveUIAdapter.render(iur, force_fallback: true)

assert heex =~ "<li"
assert heex =~ "ash-sidebar-item"
assert heex =~ "<button"
assert heex =~ "Proposals"
end

test "sidebar_item marks selected state with aria-current and is-selected class" do
iur = %{
"type" => "sidebar_item",
"id" => "sidebar-item-2",
"props" => %{"label" => "Specs", "selected?" => true},
"children" => [],
"metadata" => %{}
}

{:ok, heex} = LiveUIAdapter.render(iur, force_fallback: true)

assert heex =~ "ash-sidebar-item--selected"
assert heex =~ ~s(aria-current="page")
end

test "tabs renders tablist with tab buttons for each item" do
iur = %{
"type" => "tabs",
"id" => "tabs-1",
"props" => %{
"items" => [
%{"id" => "tab-a", "label" => "Overview"},
%{"id" => "tab-b", "label" => "Details"}
],
"active_item_id" => "tab-a"
},
"children" => [],
"metadata" => %{}
}

{:ok, heex} = LiveUIAdapter.render(iur, force_fallback: true)

assert heex =~ "ash-tabs"
assert heex =~ ~s(role="tablist")
assert heex =~ ~s(role="tab")
assert heex =~ "Overview"
assert heex =~ "Details"
assert heex =~ ~s(aria-selected="true")
end

test "tree_view renders section with ul and li nodes" do
iur = %{
"type" => "tree_view",
"id" => "tree-1",
"props" => %{
"nodes" => [
%{"id" => "node-1", "label" => "Root Node"},
%{"id" => "node-2", "label" => "Other Node"}
],
"selection_mode" => "single"
},
"children" => [],
"metadata" => %{}
}

{:ok, heex} = LiveUIAdapter.render(iur, force_fallback: true)

assert heex =~ "<section"
assert heex =~ "ash-tree-view"
assert heex =~ "<ul"
assert heex =~ "<li"
assert heex =~ "Root Node"
assert heex =~ "Other Node"
end
end
end
Loading