Reorder Sidebar Menu entries and smaller fixes #353

Merged
simon merged 7 commits from feature/reorder-sidebar-menu into main 2026-01-19 11:07:44 +01:00
7 changed files with 254 additions and 158 deletions

View file

@ -181,6 +181,29 @@
padding-left: 14px;
}
/* ============================================
Menu Groups - Disable hover and active on expanded-menu-group header
============================================ */
/* Disable all interactive effects on expanded-menu-group header (no href, not clickable)
Using [role="group"] to increase specificity and avoid !important */
.sidebar .menu > li.expanded-menu-group > div[role="group"]:not(a) {
pointer-events: none;
cursor: default;
}
/* Higher specificity selector to override DaisyUI menu hover styles
DaisyUI uses :where() which has 0 specificity, but the compiled CSS might have higher specificity
Using [role="group"] attribute selector increases specificity without !important */
.sidebar .menu > li.expanded-menu-group > div[role="group"]:not(a):hover,
.sidebar .menu > li.expanded-menu-group > div[role="group"]:not(a):active,
.sidebar .menu > li.expanded-menu-group > div[role="group"]:not(a):focus {
background-color: transparent;
box-shadow: none;
cursor: default;
color: inherit;
}
/* ============================================
Elements Only Visible in Expanded State
============================================ */
@ -217,7 +240,9 @@
- Menu has p-2 (8px), so links need 14px additional padding-left */
.sidebar .menu > li > a,
.sidebar .menu > li > button {
.sidebar .menu > li > button,
.sidebar .menu > li.expanded-menu-group > div,
.sidebar .menu > div.collapsed-menu-group > button {
@apply transition-all duration-300;
padding-left: 14px;
}
@ -226,12 +251,17 @@
- Remove gap so label (which is opacity-0 w-0) doesn't create space
- Keep padding-left at 14px so icons stay centered under logo */
[data-sidebar-expanded="false"] .sidebar .menu > li > a,
[data-sidebar-expanded="false"] .sidebar .menu > li > button {
[data-sidebar-expanded="false"] .sidebar .menu > li > button,
[data-sidebar-expanded="false"] .sidebar .menu > li.expanded-menu-group > div,
[data-sidebar-expanded="false"] .sidebar .menu > div.collapsed-menu-group > button {
@apply gap-0;
padding-left: 14px;
padding-right: 14px; /* Center icon horizontally in 64px sidebar */
}
/* ============================================
Footer Button Alignment - Left Aligned in Collapsed State
============================================ */

View file

@ -12,7 +12,7 @@ config :mv, Mv.Repo,
port: System.get_env("TEST_POSTGRES_PORT", "5000"),
database: "mv_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: System.schedulers_online() * 2
pool_size: System.schedulers_online() * 4
# We don't run a server during test. If one is required,
# you can enable the server option below.

View file

@ -75,30 +75,23 @@ defmodule MvWeb.Layouts.Sidebar do
icon="hero-users"
label={gettext("Members")}
/>
<.menu_item
href={~p"/users"}
icon="hero-user-circle"
label={gettext("Users")}
/>
<.menu_item
href={~p"/custom_field_values"}
icon="hero-rectangle-group"
label={gettext("Custom Fields")}
/>
<!-- Nested Menu: Contributions -->
<.menu_group
icon="hero-currency-dollar"
label={gettext("Contributions")}
>
<.menu_subitem href="/contribution_types" label={gettext("Contribution Types")} />
<.menu_subitem href="/membership_fee_settings" label={gettext("Settings")} />
</.menu_group>
<.menu_item
href={~p"/settings"}
icon="hero-cog-6-tooth"
label={gettext("Settings")}
href={~p"/membership_fee_types"}
icon="hero-currency-euro"
label={gettext("Fee Types")}
/>
<!-- Nested Admin Menu -->
<.menu_group icon="hero-cog-6-tooth" label={gettext("Administration")}>
<.menu_subitem href={~p"/users"} label={gettext("Users")} />
<.menu_subitem href={~p"/admin/roles"} label={gettext("Roles")} />
<.menu_subitem
href={~p"/membership_fee_settings"}
label={gettext("Fee Settings")}
/>
<.menu_subitem href={~p"/settings"} label={gettext("Settings")} />
simon marked this conversation as resolved

Nice! I really like the structure and it works also via keyboard for me now.
One idea: should we (low priority as an idea) rename Settings to Memberdata and extract the "Vereinsdaten" in a extra subitem "Verein"--> for branding etc? But more an idea for later :)

Nice! I really like the structure and it works also via keyboard for me now. One idea: should we (low priority as an idea) rename Settings to Memberdata and extract the "Vereinsdaten" in a extra subitem "Verein"--> for branding etc? But more an idea for later :)

Yeah I'm totally in for rethinking all the settings views and think about what belongs together

Yeah I'm totally in for rethinking all the settings views and think about what belongs together
</.menu_group>
</ul>
"""
end
@ -129,43 +122,41 @@ defmodule MvWeb.Layouts.Sidebar do
defp menu_group(assigns) do
~H"""
<li role="none" class="menu-group">
<!-- Expanded Mode: Details/Summary -->
<details class="expanded-menu-group">
<summary
class="flex items-center gap-3 cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
role="menuitem"
aria-haspopup="true"
>
<.icon name={@icon} class="size-5 shrink-0" aria-hidden="true" />
<span class="menu-label">{@label}</span>
</summary>
<ul role="menu" class="ml-4">
{render_slot(@inner_block)}
</ul>
</details>
<!-- Collapsed Mode: Dropdown -->
<div class="collapsed-menu-group dropdown dropdown-right">
<button
type="button"
tabindex="0"
class="flex items-center w-full p-2 rounded-lg hover:bg-base-300 tooltip tooltip-right focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
data-tip={@label}
aria-haspopup="menu"
aria-label={@label}
>
<.icon name={@icon} class="size-5" aria-hidden="true" />
</button>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box shadow-lg z-50 min-w-48 p-2 focus:outline-none"
role="menu"
>
<li class="menu-title">{@label}</li>
{render_slot(@inner_block)}
</ul>
<!-- Expanded Mode: Always open div structure -->
<li role="none" class="expanded-menu-group">
<div
class="flex items-center gap-3"
role="group"
aria-label={@label}
>
<.icon name={@icon} class="size-5 shrink-0" aria-hidden="true" />
<span class="menu-label">{@label}</span>
</div>
<ul role="menu" class="ml-4">
{render_slot(@inner_block)}
</ul>
</li>
<!-- Collapsed Mode: Dropdown -->
<div class="collapsed-menu-group dropdown dropdown-right">
<button
type="button"
tabindex="0"
class="flex items-center gap-3 px-2 py-1.5 rounded-selector hover:bg-base-300 tooltip tooltip-right focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 cursor-pointer"
role="menuitem"
data-tip={@label}
aria-haspopup="menu"
aria-label={@label}
>
<.icon name={@icon} class="size-5 shrink-0" aria-hidden="true" />
</button>
<ul
class="dropdown-content menu bg-base-100 rounded-box shadow-lg z-50 min-w-48 p-2 focus:outline-none"
role="menu"
>
<li class="menu-title">{@label}</li>
{render_slot(@inner_block)}
</ul>
</div>
"""
end

View file

@ -631,7 +631,6 @@ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}"
msgid "Please select a custom field first"
msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld"
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/member_live/form.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
@ -915,7 +914,6 @@ msgstr "Beitragsart ändern"
msgid "Contribution Start"
msgstr "Beitragsbeginn"
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/contribution_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Contribution Types"
@ -926,11 +924,6 @@ msgstr "Beitragsarten"
msgid "Contribution type"
msgstr "Beitragsart"
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Contributions"
msgstr "Beiträge"
#: lib/mv_web/live/contribution_period_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Contributions for %{name}"
@ -2188,3 +2181,28 @@ msgstr "Mitglied wurde erfolgreich erstellt"
#, elixir-autogen, elixir-format
msgid "Member updated successfully"
msgstr "Mitglied wurde erfolgreich aktualisiert"
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Roles"
msgstr "Rollen"
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Fee Settings"
msgstr "Beitragseinstellungen"
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Fee Types"
msgstr "Beitragstypen"
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Administration"
msgstr "Administration"
#~ #: lib/mv_web/components/layouts/sidebar.ex
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Contributions"
#~ msgstr "Beiträge"

View file

@ -632,7 +632,6 @@ msgstr ""
msgid "Please select a custom field first"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/member_live/form.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
@ -916,7 +915,6 @@ msgstr ""
msgid "Contribution Start"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/contribution_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Contribution Types"
@ -927,11 +925,6 @@ msgstr ""
msgid "Contribution type"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Contributions"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex
#, elixir-autogen, elixir-format
msgid "Contributions for %{name}"
@ -2189,3 +2182,23 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Member updated successfully"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Roles"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Fee Settings"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Fee Types"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Administration"
msgstr ""

View file

@ -632,7 +632,6 @@ msgstr ""
msgid "Please select a custom field first"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/member_live/form.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
@ -916,7 +915,6 @@ msgstr ""
msgid "Contribution Start"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/contribution_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Contribution Types"
@ -927,11 +925,6 @@ msgstr ""
msgid "Contribution type"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Contributions"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex
#, elixir-autogen, elixir-format
msgid "Contributions for %{name}"
@ -2189,3 +2182,33 @@ msgstr "Member created successfully"
#, elixir-autogen, elixir-format
msgid "Member updated successfully"
msgstr "Member updated successfully"
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Roles"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Fee Settings"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Fee Types"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Administration"
msgstr ""
#~ #: lib/mv_web/components/layouts/sidebar.ex
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Admin"
#~ msgstr ""
#~ #: lib/mv_web/components/layouts/sidebar.ex
#~ #, elixir-autogen, elixir-format
#~ msgid "Contributions"
#~ msgstr ""

View file

@ -122,35 +122,34 @@ defmodule MvWeb.Layouts.SidebarTest do
test "T2.2: does not render menu items when current_user is nil" do
html = render_sidebar(guest_assigns())
# Navigation links should not be rendered
refute html =~ ~s(href="/members")
refute html =~ ~s(href="/users")
refute html =~ ~s(href="/settings")
refute html =~ ~s(href="/contribution_types")
# Navigation menu should not be rendered
refute html =~ ~s(role="menubar")
refute html =~ ~s(role="menuitem")
# Footer section should not be rendered
refute html =~ "locale-select"
refute html =~ "theme-controller"
refute html =~ "locale-select"
end
test "T2.3: renders menu items when current_user is present" do
html = render_sidebar(authenticated_assigns())
# Check for Members link
assert html =~ ~s(href="/members")
# Check that menu structure exists
assert html =~ ~s(role="menubar")
assert html =~ ~s(role="menuitem")
# Check for Users link
assert html =~ ~s(href="/users")
# Check that top-level menu items exist (at least one)
# Count menu items with tooltips (top-level items have tooltips)
menu_item_count = html |> String.split("data-tip=") |> length() |> Kernel.-(1)
assert menu_item_count > 0, "Should have at least one top-level menu item"
# Check for Custom Fields link
assert html =~ ~s(href="/custom_field_values")
# Check that nested menu groups exist
assert html =~ ~s(<li role="none" class="expanded-menu-group">)
assert html =~ ~s(role="group")
assert has_class?(html, "expanded-menu-group")
# Check for Contributions section
assert html =~ ~s(href="/contribution_types")
assert html =~ ~s(href="/membership_fee_settings")
# Check for Settings link (placeholder)
assert html =~ ~s(href="/settings")
# Check that nested menu items exist
assert html =~ ~s(role="menu")
end
test "T2.4: renders sidebar with main-sidebar ID" do
@ -174,51 +173,59 @@ defmodule MvWeb.Layouts.SidebarTest do
test "T3.1: renders flat menu items with icons and labels" do
html = render_sidebar(authenticated_assigns())
# Check for Members link with icon
assert html =~ ~s(href="/members")
assert html =~ "hero-users"
# Check for Users link with icon
assert html =~ ~s(href="/users")
assert html =~ "hero-user-circle"
# Check for Custom Fields link with icon
assert html =~ ~s(href="/custom_field_values")
assert html =~ "hero-rectangle-group"
# Check for Settings link with icon
assert html =~ ~s(href="/settings")
assert html =~ "hero-cog-6-tooth"
# Check for tooltips (data-tip attribute)
# Check that top-level menu items have structure
# Top-level items have tooltips
assert html =~ "data-tip="
assert has_class?(html, "tooltip")
assert has_class?(html, "tooltip-right")
# Check that menu items have icons (hero-* classes)
assert html =~ ~r/hero-\w+/
# Check that menu items have labels
assert has_class?(html, "menu-label")
# Check that menu items have links
assert html =~ ~s(role="menuitem")
end
test "T3.2: renders nested menu with details element for expanded state" do
html = render_sidebar(authenticated_assigns())
# Check for Contributions section structure with details
assert html =~ "<details"
# Check for nested menu structure
assert html =~ ~s(<li role="none" class="expanded-menu-group">)
assert html =~ ~s(role="group")
assert html =~ ~s(aria-label="Administration")
assert has_class?(html, "expanded-menu-group")
# Check for contribution links
assert html =~ ~s(href="/contribution_types")
assert html =~ ~s(href="/membership_fee_settings")
# Check that nested menu has subitems
assert html =~ ~s(role="menu")
# Check that subitems exist (at least one link in nested menu)
# Submenu items have role="menuitem" but no data-tip attribute
# (Top-level items have data-tip, nested items don't)
# Count menuitems vs data-tips - nested items don't have data-tip
menuitem_count = html |> String.split(~s(role="menuitem")) |> length() |> Kernel.-(1)
data_tip_count = html |> String.split("data-tip=") |> length() |> Kernel.-(1)
# There should be more menuitems than data-tips (nested items don't have data-tip)
assert menuitem_count > data_tip_count,
"Should have nested menu items (menuitems without data-tip)"
end
test "T3.3: renders nested menu with dropdown for collapsed state" do
html = render_sidebar(authenticated_assigns())
# Check for collapsed dropdown container
# Check for collapsed dropdown structure
assert has_class?(html, "collapsed-menu-group")
assert has_class?(html, "dropdown")
assert has_class?(html, "dropdown-right")
# Check for dropdown-content
assert has_class?(html, "dropdown-content")
# Check for icon button
assert html =~ "hero-currency-dollar"
# Check that dropdown button has icon (any hero icon)
assert html =~ ~r/hero-\w+/
# Check ARIA attributes
assert html =~ ~s(aria-haspopup="menu")
end
end
@ -346,8 +353,9 @@ defmodule MvWeb.Layouts.SidebarTest do
test "T5.4: nested menu has correct ARIA attributes" do
html = render_sidebar(authenticated_assigns())
# Details summary should have haspopup
assert html =~ ~s(aria-haspopup="true")
# Expanded mode should have role="group" with aria-label
assert html =~ ~s(role="group")
assert html =~ ~s(aria-label="Administration")
# Dropdown button should have haspopup
assert html =~ ~s(aria-haspopup="menu")
@ -414,17 +422,17 @@ defmodule MvWeb.Layouts.SidebarTest do
test "T7.1: renders hero icons for menu items" do
html = render_sidebar(authenticated_assigns())
# Check for hero icons
assert html =~ "hero-users"
assert html =~ "hero-user-circle"
assert html =~ "hero-rectangle-group"
assert html =~ "hero-currency-dollar"
assert html =~ "hero-cog-6-tooth"
# Check that hero icons are present (pattern matching)
assert html =~ ~r/hero-\w+/
# Check that icons have aria-hidden
assert html =~ ~s(aria-hidden="true")
# Check for specific structural icons (toggle, theme) that should always exist
assert html =~ "hero-chevron-left"
assert html =~ "hero-chevron-right"
# Icons should have aria-hidden
assert html =~ ~s(aria-hidden="true")
assert html =~ "hero-sun"
assert html =~ "hero-moon"
end
test "T7.2: renders icons for theme toggle" do
@ -503,26 +511,25 @@ defmodule MvWeb.Layouts.SidebarTest do
# Header section
assert html =~ "Mila Logo"
assert html =~ ~s(src="/images/mila.svg")
# Navigation section
assert html =~ ~s(role="menubar")
assert html =~ ~s(id="main-sidebar")
# Check that menu has items (at least one top-level item)
assert html =~ ~s(role="menuitem")
# Check that nested menus exist
assert html =~ ~s(<li role="none" class="expanded-menu-group">)
assert html =~ ~s(role="group")
# Footer section
assert html =~ "theme-controller"
assert html =~ ~s(action="/set_locale")
# All expected links
expected_links = [
"/members",
"/users",
"/custom_field_values",
"/contribution_types",
"/membership_fee_settings",
"/sign-out"
]
for link <- expected_links do
assert html =~ ~s(href="#{link}"), "Missing link: #{link}"
end
# Check that critical navigation exists (at least /members)
assert html =~ ~s(href="/members"), "Critical /members route should exist"
end
end
@ -621,9 +628,10 @@ defmodule MvWeb.Layouts.SidebarTest do
test "renders expanded menu group" do
html = render_sidebar(authenticated_assigns())
# details/summary present
assert html =~ "<details"
assert html =~ "<summary"
# expanded-menu-group structure present
assert html =~ ~s(<li role="none" class="expanded-menu-group">)
assert html =~ ~s(role="group")
assert html =~ ~s(aria-label="Administration")
assert has_class?(html, "expanded-menu-group")
end
@ -639,9 +647,21 @@ defmodule MvWeb.Layouts.SidebarTest do
test "renders submenu items" do
html = render_sidebar(authenticated_assigns())
# Inner_block items rendered
assert html =~ ~s(href="/contribution_types")
assert html =~ ~s(href="/membership_fee_settings")
# Check that nested menu structure exists
assert html =~ ~s(role="menu")
# Check that subitems are rendered (links within nested menu)
# Submenu items have role="menuitem" but no data-tip attribute
# (Top-level items have data-tip, nested items don't)
# Count menuitems vs data-tips - nested items don't have data-tip
menuitem_count = html |> String.split(~s(role="menuitem")) |> length() |> Kernel.-(1)
data_tip_count = html |> String.split("data-tip=") |> length() |> Kernel.-(1)
# There should be more menuitems than data-tips (nested items don't have data-tip)
assert menuitem_count > data_tip_count,
"Should have nested menu items (menuitems without data-tip)"
# Verify nested menu structure exists
assert html =~ ~s(role="menu")
end
end
@ -821,9 +841,10 @@ defmodule MvWeb.Layouts.SidebarTest do
assert has_class?(html, "expanded-menu-group")
assert has_class?(html, "collapsed-menu-group")
# Details element should not have duplicate hover classes
# (CSS handles this, but we verify structure)
assert html =~ "<details"
# Expanded menu group should have correct structure
# (CSS handles hover effects, but we verify structure)
assert html =~ ~s(<li role="none" class="expanded-menu-group">)
assert html =~ ~s(role="group")
end
test "tooltips only visible when collapsed" do