fix: unify responsive application header
This commit is contained in:
parent
1dda417a94
commit
768d63ad5f
|
|
@ -45,6 +45,7 @@ test("public navigation does not overflow narrow or wide viewports", async ({ br
|
||||||
for (const viewport of [
|
for (const viewport of [
|
||||||
{ width: 360, height: 800 },
|
{ width: 360, height: 800 },
|
||||||
{ width: 768, height: 1024 },
|
{ width: 768, height: 1024 },
|
||||||
|
{ width: 1030, height: 900 },
|
||||||
{ width: 1440, height: 900 },
|
{ width: 1440, height: 900 },
|
||||||
]) {
|
]) {
|
||||||
const context = await newIsolatedContext(browser);
|
const context = await newIsolatedContext(browser);
|
||||||
|
|
@ -59,6 +60,36 @@ test("public navigation does not overflow narrow or wide viewports", async ({ br
|
||||||
|
|
||||||
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth);
|
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth);
|
||||||
await expect(page.getByRole("navigation", { name: "Primary navigation" })).toBeVisible();
|
await expect(page.getByRole("navigation", { name: "Primary navigation" })).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page.locator("body > header")).toHaveCount(1);
|
||||||
|
await expect(page.locator("body > nav[aria-label='Account and language']")).toHaveCount(0);
|
||||||
|
|
||||||
|
if (viewport.width < 1280) {
|
||||||
|
const menuButton = page.locator("summary[aria-label='Primary navigation']");
|
||||||
|
await expect(menuButton).toBeVisible();
|
||||||
|
await menuButton.click();
|
||||||
|
|
||||||
|
await expect(page.getByRole("link", { name: "Requests", exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "Activities", exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "Categories", exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "Helpers", exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "English", exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "Log in", exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
const openDimensions = await page.evaluate(() => ({
|
||||||
|
clientWidth: document.documentElement.clientWidth,
|
||||||
|
scrollWidth: document.documentElement.scrollWidth,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(openDimensions.scrollWidth).toBeLessThanOrEqual(openDimensions.clientWidth);
|
||||||
|
} else {
|
||||||
|
await expect(page.locator("summary[aria-label='Primary navigation']")).toBeHidden();
|
||||||
|
await expect(page.getByRole("link", { name: "Requests", exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "English", exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "Register", exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "Log in", exact: true })).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
await context.close();
|
await context.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -35,63 +35,100 @@ defmodule WhoNeedHelpWeb.Layouts do
|
||||||
|
|
||||||
def app(assigns) do
|
def app(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<header class="sticky top-0 z-30 flex flex-col items-stretch gap-2 border-b border-base-300 bg-base-100/90 px-4 py-2 backdrop-blur sm:flex-row sm:items-center sm:px-6 lg:px-8">
|
<header class="sticky top-0 z-30 border-b border-base-300 bg-base-100/90 px-4 py-2 backdrop-blur sm:px-6 lg:px-8">
|
||||||
<div class="flex-1">
|
<div class="mx-auto flex min-h-12 w-full max-w-7xl items-center justify-between gap-3">
|
||||||
<a href="/" class="flex w-fit items-center gap-3" aria-label={gettext("Who Need Help home")}>
|
<a
|
||||||
<span class="grid size-9 place-items-center rounded-xl bg-success text-xl text-success-content">+</span>
|
href="/"
|
||||||
<span class="font-black tracking-tight">Who Need Help</span>
|
class="flex min-w-0 items-center gap-3"
|
||||||
|
aria-label={gettext("Who Need Help home")}
|
||||||
|
>
|
||||||
|
<span class="grid size-9 shrink-0 place-items-center rounded-xl bg-success text-xl text-success-content">
|
||||||
|
+
|
||||||
|
</span>
|
||||||
|
<span class="truncate font-black tracking-tight">Who Need Help</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
|
||||||
<nav class="min-w-0 flex-none" aria-label={gettext("Primary navigation")}>
|
<nav class="min-w-0 flex-none" aria-label={gettext("Primary navigation")}>
|
||||||
<ul class="flex max-w-full items-center gap-1 overflow-x-auto px-1 pb-1 sm:pb-0">
|
<div class="hidden items-center gap-1 xl:flex">
|
||||||
<li>
|
<ul class="flex items-center gap-0.5">
|
||||||
<.link navigate={~p"/requests"} class="btn btn-ghost btn-sm">
|
<.primary_navigation_links current_scope={@current_scope} />
|
||||||
{gettext("Requests")}
|
</ul>
|
||||||
</.link>
|
|
||||||
</li>
|
<div
|
||||||
<li>
|
class="ml-1 flex items-center gap-1 border-l border-base-300 pl-2"
|
||||||
<.link navigate={~p"/activities"} class="btn btn-ghost btn-sm">
|
aria-label={gettext("Account and language")}
|
||||||
{gettext("Activities")}
|
>
|
||||||
</.link>
|
<.locale_switcher />
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<.link navigate={~p"/categories/proposals"} class="btn btn-ghost btn-sm">
|
|
||||||
{gettext("Categories")}
|
|
||||||
</.link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<.link navigate={~p"/leaderboard"} class="btn btn-ghost btn-sm">
|
|
||||||
{gettext("Helpers")}
|
|
||||||
</.link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<.theme_toggle />
|
<.theme_toggle />
|
||||||
</li>
|
|
||||||
<%= if @current_scope do %>
|
<%= if @current_scope do %>
|
||||||
<li :if={@current_scope.user.role in [:moderator, :admin]}>
|
<.link navigate={~p"/requests/new"} class="btn btn-primary btn-sm whitespace-nowrap">
|
||||||
<.link navigate={~p"/moderation"} class="btn btn-warning btn-sm">
|
|
||||||
{gettext("Moderation")}
|
|
||||||
</.link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<.link navigate={~p"/profile"} class="btn btn-ghost btn-sm">
|
|
||||||
{gettext("Profile")}
|
|
||||||
</.link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<.link navigate={~p"/requests/new"} class="btn btn-primary btn-sm">
|
|
||||||
{gettext("Ask for help")}
|
{gettext("Ask for help")}
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
<.account_menu current_scope={@current_scope} />
|
||||||
<% else %>
|
<% else %>
|
||||||
<li>
|
<.link navigate={~p"/users/register"} class="btn btn-ghost btn-sm">
|
||||||
|
{gettext("Register")}
|
||||||
|
</.link>
|
||||||
<.link navigate={~p"/users/log-in"} class="btn btn-primary btn-sm">
|
<.link navigate={~p"/users/log-in"} class="btn btn-primary btn-sm">
|
||||||
{gettext("Log in")}
|
{gettext("Log in")}
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details class="dropdown dropdown-end xl:hidden">
|
||||||
|
<summary
|
||||||
|
class="btn btn-ghost btn-square"
|
||||||
|
aria-label={gettext("Primary navigation")}
|
||||||
|
>
|
||||||
|
<.icon name="hero-bars-3" class="size-6" />
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div class="dropdown-content mt-3 w-[min(20rem,calc(100vw-2rem))] overflow-hidden rounded-box border border-base-300 bg-base-100 shadow-xl">
|
||||||
|
<ul class="menu w-full p-2">
|
||||||
|
<.primary_navigation_links
|
||||||
|
current_scope={@current_scope}
|
||||||
|
link_class="justify-start"
|
||||||
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-base-300 px-4 py-3">
|
||||||
|
<.locale_switcher />
|
||||||
|
<.theme_toggle />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= if @current_scope do %>
|
||||||
|
<ul class="menu w-full border-t border-base-300 p-2">
|
||||||
|
<li>
|
||||||
|
<.link navigate={~p"/profile"}>{gettext("Profile")}</.link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<.link href={~p"/users/settings"}>{gettext("Account settings")}</.link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<.link href={~p"/users/log-out"} method="delete">{gettext("Log out")}</.link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="border-t border-base-300 p-3">
|
||||||
|
<.link navigate={~p"/requests/new"} class="btn btn-primary btn-block">
|
||||||
|
{gettext("Ask for help")}
|
||||||
|
</.link>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="grid grid-cols-2 gap-2 border-t border-base-300 p-3">
|
||||||
|
<.link navigate={~p"/users/register"} class="btn btn-outline">
|
||||||
|
{gettext("Register")}
|
||||||
|
</.link>
|
||||||
|
<.link navigate={~p"/users/log-in"} class="btn btn-primary">
|
||||||
|
{gettext("Log in")}
|
||||||
|
</.link>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main id="main-content" tabindex="-1" class="px-4 py-8 outline-none sm:px-6 lg:px-8">
|
<main id="main-content" tabindex="-1" class="px-4 py-8 outline-none sm:px-6 lg:px-8">
|
||||||
|
|
@ -110,6 +147,120 @@ defmodule WhoNeedHelpWeb.Layouts do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
attr :current_scope, :map, default: nil
|
||||||
|
attr :link_class, :string, default: "btn btn-ghost btn-sm"
|
||||||
|
|
||||||
|
defp primary_navigation_links(assigns) do
|
||||||
|
~H"""
|
||||||
|
<li>
|
||||||
|
<.link navigate={~p"/requests"} class={@link_class}>
|
||||||
|
{gettext("Requests")}
|
||||||
|
</.link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<.link navigate={~p"/activities"} class={@link_class}>
|
||||||
|
{gettext("Activities")}
|
||||||
|
</.link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<.link navigate={~p"/categories/proposals"} class={@link_class}>
|
||||||
|
{gettext("Categories")}
|
||||||
|
</.link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<.link navigate={~p"/leaderboard"} class={@link_class}>
|
||||||
|
{gettext("Helpers")}
|
||||||
|
</.link>
|
||||||
|
</li>
|
||||||
|
<li :if={@current_scope && @current_scope.user.role in [:moderator, :admin]}>
|
||||||
|
<.link navigate={~p"/moderation"} class={["btn-warning", @link_class]}>
|
||||||
|
{gettext("Moderation")}
|
||||||
|
</.link>
|
||||||
|
</li>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp locale_switcher(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div
|
||||||
|
class="join"
|
||||||
|
role="group"
|
||||||
|
aria-label={gettext("Account and language")}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="?locale=en"
|
||||||
|
class={[
|
||||||
|
"btn btn-ghost btn-xs join-item",
|
||||||
|
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "en" && "btn-active"
|
||||||
|
]}
|
||||||
|
aria-label="English"
|
||||||
|
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "en" && "page"}
|
||||||
|
>
|
||||||
|
EN
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="?locale=uk"
|
||||||
|
class={[
|
||||||
|
"btn btn-ghost btn-xs join-item",
|
||||||
|
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "uk" && "btn-active"
|
||||||
|
]}
|
||||||
|
aria-label="Українська"
|
||||||
|
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "uk" && "page"}
|
||||||
|
>
|
||||||
|
UK
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="?locale=ru"
|
||||||
|
class={[
|
||||||
|
"btn btn-ghost btn-xs join-item",
|
||||||
|
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "ru" && "btn-active"
|
||||||
|
]}
|
||||||
|
aria-label="Русский"
|
||||||
|
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "ru" && "page"}
|
||||||
|
>
|
||||||
|
RU
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
attr :current_scope, :map, required: true
|
||||||
|
|
||||||
|
defp account_menu(assigns) do
|
||||||
|
~H"""
|
||||||
|
<details class="dropdown dropdown-end">
|
||||||
|
<summary class="btn btn-ghost btn-sm gap-1">
|
||||||
|
<.icon name="hero-user-circle" class="size-5" />
|
||||||
|
<span class="flex max-w-36 flex-col items-start overflow-hidden leading-tight">
|
||||||
|
<span class="max-w-full truncate">
|
||||||
|
{@current_scope.user.display_name || @current_scope.user.email}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
:if={@current_scope.user.display_name}
|
||||||
|
class="max-w-full truncate text-[0.65rem] font-normal opacity-60"
|
||||||
|
>
|
||||||
|
{@current_scope.user.email}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<.icon name="hero-chevron-down-micro" class="size-4 opacity-60" />
|
||||||
|
</summary>
|
||||||
|
<div class="dropdown-content mt-3 w-64 overflow-hidden rounded-box border border-base-300 bg-base-100 shadow-xl">
|
||||||
|
<ul class="menu w-full p-2">
|
||||||
|
<li>
|
||||||
|
<.link navigate={~p"/profile"}>{gettext("Profile")}</.link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<.link href={~p"/users/settings"}>{gettext("Account settings")}</.link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<.link href={~p"/users/log-out"} method="delete">{gettext("Log out")}</.link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Shows the flash group with standard titles and content.
|
Shows the flash group with standard titles and content.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,57 +51,6 @@
|
||||||
>
|
>
|
||||||
{gettext("Skip to main content")}
|
{gettext("Skip to main content")}
|
||||||
</a>
|
</a>
|
||||||
<nav aria-label={gettext("Account and language")}>
|
|
||||||
<ul class="menu menu-horizontal relative z-40 flex w-full flex-wrap items-center justify-end gap-2 bg-base-100 px-4 text-xs sm:px-6 lg:px-8">
|
|
||||||
<%= if @current_scope do %>
|
|
||||||
<li>
|
|
||||||
<span class="flex flex-col items-end leading-tight">
|
|
||||||
<span>{@current_scope.user.display_name || @current_scope.user.email}</span>
|
|
||||||
<span class="text-[0.65rem] text-base-content/50">{@current_scope.user.email}</span>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<.link href={~p"/profile"}>{gettext("Profile")}</.link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<.link href={~p"/users/settings"}>{gettext("Account settings")}</.link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<.link href={~p"/users/log-out"} method="delete">{gettext("Log out")}</.link>
|
|
||||||
</li>
|
|
||||||
<% else %>
|
|
||||||
<li>
|
|
||||||
<.link href={~p"/users/register"}>{gettext("Register")}</.link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<.link href={~p"/users/log-in"}>{gettext("Log in")}</.link>
|
|
||||||
</li>
|
|
||||||
<% end %>
|
|
||||||
<li class="flex flex-row gap-0">
|
|
||||||
<a
|
|
||||||
href="?locale=en"
|
|
||||||
aria-label="English"
|
|
||||||
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "en" && "page"}
|
|
||||||
>
|
|
||||||
EN
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="?locale=uk"
|
|
||||||
aria-label="Українська"
|
|
||||||
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "uk" && "page"}
|
|
||||||
>
|
|
||||||
UK
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="?locale=ru"
|
|
||||||
aria-label="Русский"
|
|
||||||
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "ru" && "page"}
|
|
||||||
>
|
|
||||||
RU
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{@inner_content}
|
{@inner_content}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,20 @@ defmodule WhoNeedHelpWeb.PageControllerTest do
|
||||||
|
|
||||||
test "GET /", %{conn: conn} do
|
test "GET /", %{conn: conn} do
|
||||||
conn = get(conn, ~p"/")
|
conn = get(conn, ~p"/")
|
||||||
assert html_response(conn, 200) =~ "Help can be closer than you think."
|
html = html_response(conn, 200)
|
||||||
|
document = LazyHTML.from_document(html)
|
||||||
|
|
||||||
assert html_response(conn, 200) =~
|
assert html =~ "Help can be closer than you think."
|
||||||
|
|
||||||
|
assert html =~
|
||||||
~s(data-map-tile-url="https://tile.openstreetmap.org/{z}/{x}/{y}.png")
|
~s(data-map-tile-url="https://tile.openstreetmap.org/{z}/{x}/{y}.png")
|
||||||
|
|
||||||
|
assert Enum.count(LazyHTML.query(document, "body > header")) == 1
|
||||||
|
|
||||||
|
assert Enum.count(LazyHTML.query(document, "header nav[aria-label='Primary navigation']")) ==
|
||||||
|
1
|
||||||
|
|
||||||
|
assert Enum.empty?(LazyHTML.query(document, "body > nav[aria-label='Account and language']"))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "GET / selects Russian locale", %{conn: conn} do
|
test "GET / selects Russian locale", %{conn: conn} do
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user