fix: improve header navigation accessibility
- Current page in nav is now a span instead of a link (no self-links) - Logo links to home page, except when already on home - Use aria-current="page" with accent underline for current page indicator - Extract logo_content, logo_inner, and nav_item helper components - Update CSS to target both a and span elements in .shop-nav This follows WCAG accessibility guidelines - links that point to the current page are confusing for screen reader users. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fe29c1ad36
commit
50d7f135bc
@ -221,17 +221,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Nav link styling with active state indicator */
|
/* Nav link styling with active state indicator */
|
||||||
.shop-nav a {
|
.shop-nav a,
|
||||||
|
.shop-nav span {
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
transition: border-color 0.2s ease, color 0.2s ease;
|
transition: border-color 0.2s ease, color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
.shop-nav a:hover {
|
||||||
color: var(--t-text-primary);
|
color: var(--t-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[aria-current="page"] {
|
.shop-nav a[aria-current="page"],
|
||||||
|
.shop-nav span[aria-current="page"] {
|
||||||
color: var(--t-text-primary);
|
color: var(--t-text-primary);
|
||||||
border-bottom-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
border-bottom-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@ -281,55 +281,25 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
|||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="shop-logo" style="display: flex; align-items: center; position: relative; z-index: 1;">
|
<div class="shop-logo" style="display: flex; align-items: center; position: relative; z-index: 1;">
|
||||||
<%= case @theme_settings.logo_mode do %>
|
<.logo_content
|
||||||
<% "text-only" -> %>
|
theme_settings={@theme_settings}
|
||||||
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
|
logo_image={@logo_image}
|
||||||
<%= @theme_settings.site_name %>
|
active_page={@active_page}
|
||||||
</span>
|
mode={@mode}
|
||||||
|
|
||||||
<% "logo-text" -> %>
|
|
||||||
<%= if @logo_image do %>
|
|
||||||
<img
|
|
||||||
src={logo_url(@logo_image, @theme_settings)}
|
|
||||||
alt={@theme_settings.site_name}
|
|
||||||
style={"height: #{@theme_settings.logo_size}px; width: auto; margin-right: 0.5rem;"}
|
|
||||||
/>
|
/>
|
||||||
<% end %>
|
|
||||||
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
|
|
||||||
<%= @theme_settings.site_name %>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<% "logo-only" -> %>
|
|
||||||
<%= if @logo_image do %>
|
|
||||||
<img
|
|
||||||
src={logo_url(@logo_image, @theme_settings)}
|
|
||||||
alt={@theme_settings.site_name}
|
|
||||||
style={"height: #{@theme_settings.logo_size}px; width: auto;"}
|
|
||||||
/>
|
|
||||||
<% else %>
|
|
||||||
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
|
|
||||||
<%= @theme_settings.site_name %>
|
|
||||||
</span>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% _ -> %>
|
|
||||||
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
|
|
||||||
<%= @theme_settings.site_name %>
|
|
||||||
</span>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="shop-nav hidden md:flex" style="gap: 1.5rem; position: relative; z-index: 1;">
|
<nav class="shop-nav hidden md:flex" style="gap: 1.5rem; position: relative; z-index: 1;">
|
||||||
<%= if @mode == :preview do %>
|
<%= if @mode == :preview do %>
|
||||||
<a href="#" phx-click="change_preview_page" phx-value-page="home" aria-current={if @active_page == "home", do: "page"} style="color: var(--t-text-secondary); text-decoration: none; cursor: pointer;">Home</a>
|
<.nav_item label="Home" page="home" active_page={@active_page} mode={:preview} />
|
||||||
<a href="#" phx-click="change_preview_page" phx-value-page="collection" aria-current={if @active_page in ["collection", "pdp"], do: "page"} style="color: var(--t-text-secondary); text-decoration: none; cursor: pointer;">Shop</a>
|
<.nav_item label="Shop" page="collection" active_page={@active_page} mode={:preview} active_pages={["collection", "pdp"]} />
|
||||||
<a href="#" phx-click="change_preview_page" phx-value-page="about" aria-current={if @active_page == "about", do: "page"} style="color: var(--t-text-secondary); text-decoration: none; cursor: pointer;">About</a>
|
<.nav_item label="About" page="about" active_page={@active_page} mode={:preview} />
|
||||||
<a href="#" phx-click="change_preview_page" phx-value-page="contact" aria-current={if @active_page == "contact", do: "page"} style="color: var(--t-text-secondary); text-decoration: none; cursor: pointer;">Contact</a>
|
<.nav_item label="Contact" page="contact" active_page={@active_page} mode={:preview} />
|
||||||
<% else %>
|
<% else %>
|
||||||
<a href="/" aria-current={if @active_page == "home", do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">Home</a>
|
<.nav_item label="Home" href="/" active_page={@active_page} page="home" />
|
||||||
<a href="/collections/all" aria-current={if @active_page in ["collection", "pdp"], do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">Shop</a>
|
<.nav_item label="Shop" href="/collections/all" active_page={@active_page} page="collection" active_pages={["collection", "pdp"]} />
|
||||||
<a href="/about" aria-current={if @active_page == "about", do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">About</a>
|
<.nav_item label="About" href="/about" active_page={@active_page} page="about" />
|
||||||
<a href="/contact" aria-current={if @active_page == "contact", do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">Contact</a>
|
<.nav_item label="Contact" href="/contact" active_page={@active_page} page="contact" />
|
||||||
<% end %>
|
<% end %>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@ -375,6 +345,83 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
|||||||
|
|
||||||
defp logo_url(logo_image, _), do: "/images/#{logo_image.id}"
|
defp logo_url(logo_image, _), do: "/images/#{logo_image.id}"
|
||||||
|
|
||||||
|
# Logo content that links to home, except when already on home page.
|
||||||
|
# This follows accessibility best practices - current page should not be a link.
|
||||||
|
attr :theme_settings, :map, required: true
|
||||||
|
attr :logo_image, :map, default: nil
|
||||||
|
attr :active_page, :string, default: nil
|
||||||
|
attr :mode, :atom, default: :live
|
||||||
|
|
||||||
|
defp logo_content(assigns) do
|
||||||
|
is_home = assigns.active_page == "home"
|
||||||
|
assigns = assign(assigns, :is_home, is_home)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<%= if @is_home do %>
|
||||||
|
<.logo_inner theme_settings={@theme_settings} logo_image={@logo_image} />
|
||||||
|
<% else %>
|
||||||
|
<%= if @mode == :preview do %>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
phx-click="change_preview_page"
|
||||||
|
phx-value-page="home"
|
||||||
|
style="display: flex; align-items: center; text-decoration: none;"
|
||||||
|
>
|
||||||
|
<.logo_inner theme_settings={@theme_settings} logo_image={@logo_image} />
|
||||||
|
</a>
|
||||||
|
<% else %>
|
||||||
|
<a href="/" style="display: flex; align-items: center; text-decoration: none;">
|
||||||
|
<.logo_inner theme_settings={@theme_settings} logo_image={@logo_image} />
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
attr :theme_settings, :map, required: true
|
||||||
|
attr :logo_image, :map, default: nil
|
||||||
|
|
||||||
|
defp logo_inner(assigns) do
|
||||||
|
~H"""
|
||||||
|
<%= case @theme_settings.logo_mode do %>
|
||||||
|
<% "text-only" -> %>
|
||||||
|
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
|
||||||
|
<%= @theme_settings.site_name %>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<% "logo-text" -> %>
|
||||||
|
<%= if @logo_image do %>
|
||||||
|
<img
|
||||||
|
src={logo_url(@logo_image, @theme_settings)}
|
||||||
|
alt={@theme_settings.site_name}
|
||||||
|
style={"height: #{@theme_settings.logo_size}px; width: auto; margin-right: 0.5rem;"}
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
|
||||||
|
<%= @theme_settings.site_name %>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<% "logo-only" -> %>
|
||||||
|
<%= if @logo_image do %>
|
||||||
|
<img
|
||||||
|
src={logo_url(@logo_image, @theme_settings)}
|
||||||
|
alt={@theme_settings.site_name}
|
||||||
|
style={"height: #{@theme_settings.logo_size}px; width: auto;"}
|
||||||
|
/>
|
||||||
|
<% else %>
|
||||||
|
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
|
||||||
|
<%= @theme_settings.site_name %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% _ -> %>
|
||||||
|
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
|
||||||
|
<%= @theme_settings.site_name %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
defp header_background_style(settings, header_image) do
|
defp header_background_style(settings, header_image) do
|
||||||
"position: absolute; top: 0; left: 0; right: 0; bottom: 0; " <>
|
"position: absolute; top: 0; left: 0; right: 0; bottom: 0; " <>
|
||||||
"background-image: url('/images/#{header_image.id}'); " <>
|
"background-image: url('/images/#{header_image.id}'); " <>
|
||||||
@ -383,6 +430,45 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
|||||||
"background-repeat: no-repeat; z-index: 0;"
|
"background-repeat: no-repeat; z-index: 0;"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Navigation item that renders as a span (not a link) when on the current page.
|
||||||
|
# This follows accessibility best practices - current page should not be a link.
|
||||||
|
attr :label, :string, required: true
|
||||||
|
attr :page, :string, required: true
|
||||||
|
attr :active_page, :string, required: true
|
||||||
|
attr :href, :string, default: nil
|
||||||
|
attr :mode, :atom, default: :live
|
||||||
|
attr :active_pages, :list, default: nil
|
||||||
|
|
||||||
|
defp nav_item(assigns) do
|
||||||
|
# Allow matching multiple pages (e.g., "Shop" is active for both collection and pdp)
|
||||||
|
active_pages = assigns.active_pages || [assigns.page]
|
||||||
|
is_current = assigns.active_page in active_pages
|
||||||
|
assigns = assign(assigns, :is_current, is_current)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<%= if @is_current do %>
|
||||||
|
<span aria-current="page" style="color: var(--t-text-secondary); text-decoration: none;">
|
||||||
|
{@label}
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<%= if @mode == :preview do %>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
phx-click="change_preview_page"
|
||||||
|
phx-value-page={@page}
|
||||||
|
style="color: var(--t-text-secondary); text-decoration: none; cursor: pointer;"
|
||||||
|
>
|
||||||
|
{@label}
|
||||||
|
</a>
|
||||||
|
<% else %>
|
||||||
|
<a href={@href} style="color: var(--t-text-secondary); text-decoration: none;">
|
||||||
|
{@label}
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Renders the cart drawer (floating sidebar).
|
Renders the cart drawer (floating sidebar).
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user