mitgliederverwaltung/lib/mv_web/live/components/view_settings_dropdown_component.ex

130 lines
3.9 KiB
Elixir

defmodule MvWeb.Components.ViewSettingsDropdownComponent do
@moduledoc """
LiveComponent for the member-overview view settings (§1.20).
Renders an icon button with a tooltip that opens a dropdown of toggles:
* Compact mode — table row density (compact ↔ comfortable)
* Compact Member field — composite last+first name cell, with a sub-toggle
"include email"
* Compact address field — composite street / postal+city cell
Turning a composite field off surfaces the underlying separate columns in the
table. The component is display-only: it emits `{:view_setting_toggled, key}`
to the parent LiveView, which owns and persists the settings.
## Props
- `:density` — `:compact` or `:comfortable`
- `:compact_member` — boolean
- `:member_include_email` — boolean
- `:compact_address` — boolean
- `:id` — component id
"""
use MvWeb, :live_component
@impl true
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_new(:open, fn -> false end)}
end
@impl true
def render(assigns) do
~H"""
<div>
<.dropdown_menu
id="view-settings-menu"
icon="hero-cog-6-tooth"
button_label={gettext("View")}
open={@open}
phx_target={@myself}
menu_width="w-72"
testid="view-settings"
button_testid="view-settings-button"
>
<li role="none" class="mb-1 px-2">
<span class="font-semibold">{gettext("View settings")}</span>
</li>
<li role="separator" class="divider my-1"></li>
<.toggle_item
setting="density"
label={gettext("Compact mode")}
checked={@density == :compact}
target={@myself}
/>
<.toggle_item
setting="compact_member"
label={gettext("Compact Member field")}
checked={@compact_member}
target={@myself}
/>
<%!-- The email line lives inside the composite Member cell, so this
sub-toggle is only meaningful while that cell is compact. When the
composite is off, email is its own column and this toggle is omitted. --%>
<li :if={@compact_member} role="none" class="pl-6">
<.toggle_item
setting="member_include_email"
label={gettext("include email")}
checked={@member_include_email}
target={@myself}
/>
</li>
<.toggle_item
setting="compact_address"
label={gettext("Compact address field")}
checked={@compact_address}
target={@myself}
/>
</.dropdown_menu>
</div>
"""
end
attr :setting, :string, required: true
attr :label, :string, required: true
attr :checked, :boolean, required: true
attr :target, :any, required: true
defp toggle_item(assigns) do
~H"""
<button
type="button"
role="menuitemcheckbox"
aria-checked={to_string(@checked)}
aria-label={@label}
tabindex="0"
class="flex items-center gap-2 px-2 py-1 rounded w-full text-left cursor-pointer hover:bg-base-200"
phx-click="toggle"
phx-value-setting={@setting}
phx-target={@target}
data-testid={"view-setting-#{String.replace(@setting, "_", "-")}"}
>
<span class={if @checked, do: "text-primary", else: "text-base-300"} aria-hidden="true">
<.icon name="hero-check" class="size-4 shrink-0" />
</span>
<span>{@label}</span>
</button>
"""
end
@impl true
def handle_event("toggle_dropdown", _params, socket) do
{:noreply, assign(socket, :open, !socket.assigns.open)}
end
def handle_event("close_dropdown", _params, socket) do
{:noreply, assign(socket, :open, false)}
end
def handle_event("toggle", %{"setting" => setting}, socket) do
send(self(), {:view_setting_toggled, String.to_existing_atom(setting)})
{:noreply, socket}
end
end