Skip to content

Commit 6b867b2

Browse files
authored
Merge pull request #3 from plausible/code-examples
Better code examples for dropdowns
2 parents 620704b + da63141 commit 6b867b2

File tree

12 files changed

+402
-136
lines changed

12 files changed

+402
-136
lines changed

lib/prima/dropdown.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ defmodule Prima.Dropdown do
33
alias Phoenix.LiveView.JS
44

55
attr :id, :string, default: ""
6+
attr :rest, :global
67
slot :inner_block, required: true
78

89
def dropdown(assigns) do
910
~H"""
10-
<div id={@id} phx-hook="Dropdown" phx-click-away={JS.dispatch("prima:close")}>
11+
<div id={@id} phx-hook="Dropdown" phx-click-away={JS.dispatch("prima:close")} {@rest}>
1112
{render_slot(@inner_block)}
1213
</div>
1314
"""

lib/prima_web.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ defmodule PrimaWeb do
6262
import Phoenix.HTML
6363
# Core UI components and translation
6464
import PrimaWeb.CoreComponents
65+
import PrimaWeb.CodeExample
6566

6667
# Shortcut for generating JS commands
6768
alias Phoenix.LiveView.JS
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
defmodule PrimaWeb.CodeExample do
2+
@moduledoc """
3+
A component for displaying live demos alongside their source code.
4+
5+
Renders the component example in a tabbed interface with Preview and Code views.
6+
The component can either render provided content via inner_block or dynamically
7+
render HEEx templates from files.
8+
"""
9+
use Phoenix.Component
10+
alias Phoenix.LiveView.JS
11+
12+
attr :file, :string, required: true, doc: "Path to file in priv/code_examples/"
13+
14+
@doc """
15+
Displays a live demo alongside syntax-highlighted source code.
16+
17+
Renders the component example with a tabbed interface for switching between
18+
Preview and Code views. The source code is loaded from a file in priv/code_examples/
19+
and automatically rendered in the preview.
20+
21+
## Example
22+
23+
<.code_example file="dropdown/basic_dropdown.heex" />
24+
"""
25+
def code_example(assigns) do
26+
source = get_code_source(assigns)
27+
assigns = assign(assigns, :highlighted_code, highlight_code(source))
28+
assigns = assign(assigns, :rendered_content, render_heex_content(source, assigns))
29+
id = assigns[:id] || "code-example-#{:erlang.unique_integer([:positive])}"
30+
assigns = assign(assigns, :id, id)
31+
32+
~H"""
33+
<div class="relative border border-gray-200 rounded-lg bg-white" id={@id}>
34+
<div class="flex items-center justify-between border-b border-gray-200 rounded-t-lg px-4 py-2 bg-gray-50">
35+
<div class="flex gap-2">
36+
<button
37+
type="button"
38+
phx-click={
39+
JS.add_class("hidden", to: "##{@id}-code")
40+
|> JS.remove_class("hidden", to: "##{@id}-preview")
41+
|> JS.add_class("bg-white border-gray-300 text-gray-900", to: "##{@id}-preview-tab")
42+
|> JS.remove_class("bg-white border-gray-300 text-gray-900", to: "##{@id}-code-tab")
43+
|> JS.add_class("bg-gray-50 border-transparent text-gray-600", to: "##{@id}-code-tab")
44+
|> JS.remove_class("bg-gray-50 border-transparent text-gray-600",
45+
to: "##{@id}-preview-tab"
46+
)
47+
}
48+
id={"#{@id}-preview-tab"}
49+
class="px-3 py-1.5 text-sm font-medium rounded border bg-white border-gray-300 text-gray-900 transition-colors"
50+
>
51+
Preview
52+
</button>
53+
<button
54+
type="button"
55+
phx-click={
56+
JS.add_class("hidden", to: "##{@id}-preview")
57+
|> JS.remove_class("hidden", to: "##{@id}-code")
58+
|> JS.add_class("bg-white border-gray-300 text-gray-900", to: "##{@id}-code-tab")
59+
|> JS.remove_class("bg-white border-gray-300 text-gray-900", to: "##{@id}-preview-tab")
60+
|> JS.add_class("bg-gray-50 border-transparent text-gray-600",
61+
to: "##{@id}-preview-tab"
62+
)
63+
|> JS.remove_class("bg-gray-50 border-transparent text-gray-600",
64+
to: "##{@id}-code-tab"
65+
)
66+
}
67+
id={"#{@id}-code-tab"}
68+
class="px-3 py-1.5 text-sm font-medium rounded border bg-gray-50 border-transparent text-gray-600 transition-colors"
69+
>
70+
Code
71+
</button>
72+
</div>
73+
</div>
74+
75+
<div id={"#{@id}-preview"} class="p-6">
76+
{Phoenix.HTML.raw(@rendered_content)}
77+
</div>
78+
79+
<div id={"#{@id}-code"} class="hidden">
80+
<!-- <div class="p-4 bg-gray-900 rounded-b-lg overflow-x-auto text-sm"> -->
81+
{Phoenix.HTML.raw(@highlighted_code)}
82+
<!-- </div> -->
83+
</div>
84+
</div>
85+
"""
86+
end
87+
88+
defp get_code_source(%{file: file}) when is_binary(file) do
89+
file_path = Path.join(["priv", "code_examples", file])
90+
91+
case File.read(file_path) do
92+
{:ok, content} -> content
93+
{:error, _} -> "Error: Could not read file '#{file}'"
94+
end
95+
end
96+
97+
defp highlight_code(source) do
98+
source
99+
|> String.trim()
100+
|> Autumn.highlight!(
101+
language: "heex",
102+
formatter:
103+
{:html_inline, theme: "molokai", pre_class: "p-4 rounded-b-lg overflow-x-auto text-sm"}
104+
)
105+
end
106+
107+
defp render_heex_content(template_string, assigns) do
108+
try do
109+
# Compile and evaluate the HEEx template with component imports
110+
{result, _} =
111+
Code.eval_string(
112+
"""
113+
import Phoenix.Component
114+
import Prima.Modal
115+
import Prima.Dropdown
116+
import Prima.Combobox
117+
import PrimaWeb.CoreComponents
118+
119+
~H\"\"\"
120+
#{template_string}
121+
\"\"\"
122+
""",
123+
assigns: assigns
124+
)
125+
126+
{:safe, result}
127+
rescue
128+
e ->
129+
"<div class='text-red-600 p-4'>Error rendering template: #{Exception.message(e)}</div>"
130+
end
131+
end
132+
end

lib/prima_web/live/demo_live/dropdown_page.html.heex

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,10 @@
1717
elements.
1818
</p>
1919

20-
<div class="not-prose bg-white border border-gray-200 rounded-lg p-6 my-6">
21-
<.dropdown_demo />
20+
<div class="not-prose">
21+
<.code_example file="dropdown/basic.html.heex" />
2222
</div>
2323

24-
<Prima.CodeBlock.code_block file="dropdown/basic_dropdown.heex" />
25-
2624
<p>
2725
The dropdown component automatically handles opening/closing, keyboard navigation, and focus management. Click the trigger or use Enter/Space to open, then navigate with arrow keys.
2826
</p>
@@ -33,12 +31,10 @@
3331
attribute. Disabled items cannot be focused via keyboard navigation, and do not close the menu when clicked. They remain accessible to screen readers following ARIA accessibility standards.
3432
</p>
3533

36-
<div class="not-prose bg-white border border-gray-200 rounded-lg p-6 my-6">
37-
<.disabled_dropdown_demo />
34+
<div class="not-prose">
35+
<.code_example file="dropdown/disabled.html.heex" />
3836
</div>
3937

40-
<Prima.CodeBlock.code_block file="dropdown/disabled_dropdown.heex" />
41-
4238
<p>
4339
Disabled items use <code>aria-disabled="true"</code>
4440
and <code>data-disabled="true"</code>
@@ -56,17 +52,15 @@
5652
variant applies when an item has focus (via keyboard navigation or hover), responding to keyboard navigation and hover states.
5753
</p>
5854

59-
<div class="not-prose bg-white border border-gray-200 rounded-lg p-6 my-6">
60-
<.styled_dropdown_demo />
55+
<div class="not-prose">
56+
<.code_example file="dropdown/styled.html.heex" />
6157
</div>
6258

6359
<p>
6460
Items without focus use their default styling, while items with the <code>data-focus</code>
6561
attribute get the enhanced styling. This creates a clear visual indication of the currently focused item without requiring separate styles for unfocused states.
6662
</p>
6763

68-
<Prima.CodeBlock.code_block file="dropdown/styled_dropdown.heex" />
69-
7064
<p>
7165
You can customize the focused state styling by modifying the <code>data-focus:</code>
7266
classes. Common patterns include background color changes, text color variations, and subtle animations to enhance the user experience.
@@ -77,8 +71,8 @@
7771
Control dropdown positioning using standard CSS positioning classes. The component supports various alignment options and automatically handles menu overflow.
7872
</p>
7973

80-
<div class="not-prose bg-white border border-gray-200 rounded-lg p-6 my-6">
81-
<.positioned_dropdown_demo />
74+
<div class="not-prose">
75+
<.code_example file="dropdown/positioned.html.heex" />
8276
</div>
8377

8478
<p>
@@ -87,8 +81,6 @@
8781
to control the transform origin for smooth animations. Combine with <code>left-0</code>, <code>right-0</code>, or other positioning utilities as needed.
8882
</p>
8983

90-
<Prima.CodeBlock.code_block file="dropdown/positioned_dropdown.heex" />
91-
9284
<h2>Keyboard Interaction</h2>
9385
<p>
9486
The dropdown component provides full keyboard accessibility with comprehensive navigation support. All interactions follow standard ARIA patterns for menu components. Try typing letters like "a", "b", or "d" when the menu is open to see typeahead search in action.

lib/prima_web/live/demo_live/dropdown_demo.html.heex renamed to priv/code_examples/dropdown/basic.html.heex

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
<div class="relative inline-block text-left">
2-
<.dropdown id="demo-dropdown">
1+
<.dropdown id="demo-dropdown" class="relative inline-block">
32
<.dropdown_trigger as={&button/1}>
43
Open Dropdown
54
<svg
@@ -15,8 +14,7 @@
1514
/>
1615
</svg>
1716
</.dropdown_trigger>
18-
<.dropdown_menu class="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
19-
<div class="py-1" role="none">
17+
<.dropdown_menu class="absolute right-0 z-10 mt-2 w-56 py-1 origin-top-right rounded-md bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
2018
<.dropdown_item class="text-gray-700 data-focus:bg-gray-100 data-focus:text-gray-900 block px-4 py-2 text-sm">
2119
Account settings
2220
</.dropdown_item>
@@ -32,7 +30,5 @@
3230
<.dropdown_item class="text-gray-700 data-focus:bg-gray-100 data-focus:text-gray-900 block px-4 py-2 text-sm">
3331
Sign out
3432
</.dropdown_item>
35-
</div>
3633
</.dropdown_menu>
3734
</.dropdown>
38-
</div>

priv/code_examples/dropdown/basic_dropdown.heex

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<div class="relative inline-block text-left">
2+
<.dropdown id="disabled-demo-dropdown">
3+
<.dropdown_trigger as={&button/1}>
4+
Open Dropdown
5+
<svg
6+
class="-mr-1 h-5 w-5 text-whites"
7+
viewBox="0 0 20 20"
8+
fill="currentColor"
9+
aria-hidden="true"
10+
>
11+
<path
12+
fill-rule="evenodd"
13+
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
14+
clip-rule="evenodd"
15+
/>
16+
</svg>
17+
</.dropdown_trigger>
18+
<.dropdown_menu class="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
19+
<div class="py-1" role="none">
20+
<.dropdown_item class="text-gray-700 data-focus:bg-gray-100 data-focus:text-gray-900 block px-4 py-2 text-sm">
21+
Edit Profile
22+
</.dropdown_item>
23+
<.dropdown_item
24+
disabled={true}
25+
class="text-gray-400 data-disabled:opacity-50 block px-4 py-2 text-sm"
26+
>
27+
Archive (disabled)
28+
</.dropdown_item>
29+
<.dropdown_item class="text-gray-700 data-focus:bg-gray-100 data-focus:text-gray-900 block px-4 py-2 text-sm">
30+
Settings
31+
</.dropdown_item>
32+
<.dropdown_item class="text-red-700 data-focus:bg-red-100 data-focus:text-red-900 block px-4 py-2 text-sm">
33+
Sign Out
34+
</.dropdown_item>
35+
</div>
36+
</.dropdown_menu>
37+
</.dropdown>
38+
</div>

priv/code_examples/dropdown/disabled_dropdown.heex

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)