Packages
noora
0.82.6
0.85.0
0.84.1
0.84.0
0.83.2
0.83.1
0.83.0
0.82.7
0.82.6
0.82.5
0.82.4
0.82.3
0.82.2
0.82.1
0.82.0
0.81.4
0.81.3
0.81.2
0.81.1
0.81.0
0.80.1
0.80.0
0.79.1
0.79.0
0.78.1
0.78.0
0.77.4
0.77.3
0.77.2
0.77.1
0.77.0
0.76.0
0.75.0
0.74.0
0.73.0
0.72.0
0.71.0
0.70.0
0.69.1
0.69.0
0.68.0
0.67.0
0.66.0
0.65.0
0.64.2
0.64.1
0.64.0
0.63.2
0.63.1
0.63.0
0.62.0
0.61.0
0.60.0
0.59.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.0
0.54.0
0.53.1
0.53.0
0.52.1
0.52.0
0.51.0
0.50.1
0.50.0
0.49.0
0.48.0
0.47.0
0.46.0
0.45.0
0.44.3
0.44.2
0.44.1
0.44.0
0.43.0
0.42.0
0.41.0
0.40.6
0.40.5
0.40.4
0.40.3
0.40.2
0.40.1
0.40.0
0.39.1
0.39.0
0.38.0
0.37.0
0.36.0
0.35.0
0.34.0
0.33.0
0.32.1
0.32.0
0.31.0
0.30.0
0.29.2
0.29.1
0.29.0
0.28.5
0.28.4
0.28.3
0.28.2
0.28.1
0.28.0
0.27.0
0.26.1
0.26.0
0.25.0
0.24.0
0.23.1
0.23.0
0.22.1
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.1
0.12.0
0.11.2
0.11.1
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.1
0.6.0
0.5.1
0.5.0
0.4.0
0.3.2
0.3.1
0.3.0
0.2.2
0.2.0
0.1.0
0.1.0-rc.2
0.1.0-rc.1
0.1.0-alpha.6
0.1.0-alpha.5
0.1.0-alpha.4
0.1.0-alpha.3
0.1.0-alpha.2
0.1.0-alpha.1
A component library for Phoenix LiveView applications
Current section
Files
Jump to
Current section
Files
lib/noora/dropdown.ex
defmodule Noora.Dropdown do
@moduledoc """
Renders a customizable dropdown component with a trigger, menu, and item options, supporting icons, labels, and event handling.
## Example
```elixir
<.dropdown id="menu" label="Options">
<.dropdown_item label="Edit" navigate="/edit" />
<.dropdown_item label="Delete" on_click="delete" />
</.dropdown>
```
"""
use Phoenix.Component
import Noora.CheckboxControl
import Noora.Icon
import Noora.LineDivider
import Noora.Utils
attr(:id, :string, required: true, doc: "Unique identifier for the dropdown component")
attr(:label, :string, default: nil, doc: "Main text displayed in the dropdown trigger")
attr(:size, :string,
values: ~w(medium large),
default: "large",
doc: "The size of the dropdown trigger"
)
attr(:secondary_text, :string,
default: nil,
doc: "Secondary text displayed to the left of the main label"
)
attr(:icon_only, :boolean, default: false, doc: "Whether the dropdown trigger is icon-only")
attr(:hint, :string, default: nil, doc: "Hint text for the dropdown")
attr(:disabled, :boolean, default: nil, doc: "Whether the dropdown is disabled")
attr(:on_open_change, :string, default: nil, doc: "Event handler for when the dropdown opens")
attr(:on_highlight_change, :string,
default: nil,
doc: "Event handler for when the highlighted option changes"
)
attr(:on_select, :string, default: nil, doc: "Event handler for when an option is selected")
attr(:on_escape_key_down, :string,
default: nil,
doc: "Event handler for when the escape key is pressed"
)
attr(:on_pointer_down_outside, :string,
default: nil,
doc: "Event handler for when the pointer is pressed outside the dropdown"
)
attr(:on_focus_outside, :string,
default: nil,
doc: "Function called when the focus is moved outside the component"
)
attr(:on_interact_outside, :string,
default: nil,
doc: "Function called when an interaction happens outside the component"
)
attr(:close_on_select, :boolean,
default: true,
doc: "Whether to close the dropdown when an item is selected"
)
attr(:rest, :global, doc: "Additional HTML attributes")
slot(:icon, doc: "Icon to be rendered in the dropdown trigger")
slot(:search, doc: "Search input to be rendered at the top of the dropdown menu (outside scroll area)")
slot(:inner_block, doc: "Content to be rendered inside the dropdown menu")
def dropdown(assigns) do
~H"""
<.portal id={@id <> "-content-portal"} target={"#" <> @id <> "-content-target"}>
{render_slot(@inner_block)}
</.portal>
<.portal :if={!@icon_only} id={@id <> "-label-portal"} target={"#" <> @id <> "-label-target"}>
{@label}
</.portal>
<.portal
:if={!@icon_only and has_slot_content?(@icon, assigns)}
id={@id <> "-icon-portal"}
target={"#" <> @id <> "-icon-target"}
>
{render_slot(@icon)}
</.portal>
<div
id={@id}
class="noora-dropdown"
phx-hook="NooraDropdown"
phx-update="ignore"
data-size={@size}
data-loop-focus
data-typeahead
data-close-on-select={to_string(@close_on_select)}
data-on-open-change={@on_open_change}
data-on-highlight-change={@on_highlight_change}
data-on-select={@on_select}
data-on-escape-key-down={@on_escape_key_down}
data-on-pointer-down-outside={@on_pointer_down_outside}
data-on-focus-outside={@on_focus_outside}
data-on-interact-outside={@on_interact_outside}
{@rest}
>
<button :if={!@icon_only} data-part="trigger" disabled={@disabled} type="button">
<div data-part="label-wrapper">
<div :if={has_slot_content?(@icon, assigns)} data-part="icon">
<span id={@id <> "-icon-target"}></span>
</div>
<span :if={@secondary_text} data-part="secondary-text">
{@secondary_text}
</span>
<span data-part="label"><span id={@id <> "-label-target"}></span></span>
</div>
<div data-part="indicator">
<div data-part="indicator-down">
<.chevron_down />
</div>
<div data-part="indicator-up">
<.chevron_up />
</div>
</div>
</button>
<button :if={@icon_only} data-part="trigger" data-icon-only disabled={@disabled} type="button">
<div data-part="icon">
{render_slot(@icon)}
</div>
</button>
<div data-part="positioner">
<div class="noora-dropdown-content" data-part="content">
<div :if={has_slot_content?(@search, assigns)} data-part="search">
{render_slot(@search)}
</div>
<div id={@id <> "-content-target"} data-part="items"></div>
</div>
</div>
<span :if={@hint} data-part="hint">
<.info_circle />
<span>{@hint}</span>
</span>
</div>
"""
end
attr(:id, :string, required: true, doc: "Unique identifier for the dropdown component")
attr(:label, :string, default: nil, doc: "Main text displayed in the dropdown trigger")
attr(:size, :string, values: ~w(small large), default: "small", doc: "The size of the inline dropdown trigger")
attr(:disabled, :boolean, default: nil, doc: "Whether the dropdown is disabled")
attr(:on_open_change, :string, default: nil, doc: "Event handler for when the dropdown opens")
attr(:on_highlight_change, :string,
default: nil,
doc: "Event handler for when the highlighted option changes"
)
attr(:on_select, :string, default: nil, doc: "Event handler for when an option is selected")
attr(:on_escape_key_down, :string,
default: nil,
doc: "Event handler for when the escape key is pressed"
)
attr(:on_pointer_down_outside, :string,
default: nil,
doc: "Event handler for when the pointer is pressed outside the dropdown"
)
attr(:on_focus_outside, :string,
default: nil,
doc: "Function called when the focus is moved outside the component"
)
attr(:on_interact_outside, :string,
default: nil,
doc: "Function called when an interaction happens outside the component"
)
slot(:icon, doc: "Icon to be rendered in the dropdown trigger")
slot(:inner_block, doc: "Content to be rendered inside the dropdown menu")
attr(:rest, :global, doc: "Additional HTML attributes")
def inline_dropdown(assigns) do
~H"""
<div
id={@id}
class="noora-inline-dropdown"
phx-hook="NooraDropdown"
data-size={@size}
data-loop-focus
data-close-on-select
data-typeahead
data-on-open-change={@on_open_change}
data-on-highlight-change={@on_highlight_change}
data-on-select={@on_select}
data-on-escape-key-down={@on_escape_key_down}
data-on-pointer-down-outside={@on_pointer_down_outside}
data-on-focus-outside={@on_focus_outside}
data-on-interact-outside={@on_interact_outside}
{@rest}
>
<button data-part="trigger" disabled={@disabled}>
<div :if={has_slot_content?(@icon, assigns)} data-part="icon">
{render_slot(@icon)}
</div>
<span data-part="label">{@label}</span>
<div data-part="indicator">
<div data-part="indicator-down">
<.chevron_down />
</div>
<div data-part="indicator-up">
<.chevron_up />
</div>
</div>
</button>
<div data-part="positioner">
<div class="noora-dropdown-content" data-part="content">
{render_slot(@inner_block)}
</div>
</div>
</div>
"""
end
attr(:value, :string, required: false, doc: "Value associated with the dropdown item")
attr(:on_click, :string,
default: nil,
doc: "Event handler for when the dropdown item is clicked"
)
attr(:patch, :string, default: nil, doc: "Phoenix LiveView patch navigation path")
attr(:navigate, :string, default: nil, doc: "Phoenix LiveView navigation path")
attr(:href, :string, default: nil, doc: "Standard URL for navigation")
attr(:size, :string, values: ~w(small large), default: "small", doc: "Size of the dropdown item")
attr(:label, :string, default: nil, doc: "Text displayed as the main content of the item")
attr(:secondary_text, :string,
default: nil,
doc: "Secondary text displayed in parentheses after the label"
)
attr(:description, :string,
default: nil,
doc: "Additional description text (only visible when size is 'large')"
)
attr(:checked, :boolean,
default: nil,
doc:
"When set to true or false, renders a checkbox in the checked or unchecked state. When nil (default), no checkbox is rendered."
)
slot(:right_icon,
required: false,
doc: "Optional slot for rendering an icon on the right side of the item"
)
attr(:rest, :global, doc: "Additional HTML attributes")
slot(:left_icon,
required: false,
doc: "Optional slot for rendering an icon on the left side of the item"
)
def dropdown_item(assigns) do
~H"""
<%= if is_nil(@patch) and is_nil(@navigate) and is_nil(@href) do %>
<span
class="noora-dropdown-item"
data-part="item"
data-value={@value || @label}
data-label={@label || @value}
phx-click={@on_click}
phx-value-data={@value}
data-size={@size}
data-checkbox={not is_nil(@checked)}
phx-hook={if not is_nil(@checked), do: "NooraDropdownCheckbox"}
id={if not is_nil(@checked), do: (@value || @label) <> "-checkbox-item"}
{@rest}
>
<div :if={not is_nil(@checked)} data-part="checkbox">
<.checkbox_control checked={@checked} />
</div>
<div :if={has_slot_content?(@left_icon, assigns) and is_nil(@checked)} data-part="left-icon">
{render_slot(@left_icon)}
</div>
<div data-part="content">
<div data-part="body">
<span :if={!@label} data-part="label">
{render_slot(@inner_block)}
</span>
<span :if={@label} data-part="label">{@label}</span>
<span :if={@secondary_text} data-part="secondary-text">
({@secondary_text})
</span>
<div :if={@size == "large"}>
<span data-part="description">
{@description}
</span>
</div>
</div>
</div>
<div :if={has_slot_content?(@right_icon, assigns)} data-part="right-icon">
{render_slot(@right_icon)}
</div>
</span>
<% else %>
<.link
class="noora-dropdown-item"
data-part="item"
data-value={@value || @label}
data-label={@label || @value}
phx-click={@on_click}
phx-value-data={@value}
patch={@patch}
navigate={@navigate}
href={@href}
data-size={@size}
{@rest}
>
<div :if={has_slot_content?(@left_icon, assigns)} data-part="left-icon">
{render_slot(@left_icon)}
</div>
<div data-part="content">
<div data-part="body">
<span :if={!@label} data-part="label">
{render_slot(@inner_block)}
</span>
<span :if={@label} data-part="label">{@label}</span>
<span :if={@secondary_text} data-part="secondary-text">
({@secondary_text})
</span>
<div :if={@size == "large"}>
<span data-part="description">
{@description}
</span>
</div>
</div>
</div>
<div :if={has_slot_content?(@right_icon, assigns)} data-part="right-icon">
{render_slot(@right_icon)}
</div>
</.link>
<% end %>
"""
end
def dropdown_separator(assigns) do
~H"""
<.line_divider data-part="separator" />
"""
end
end