diff --git a/PROGRESS.md b/PROGRESS.md index 29bd857..6d77f15 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -64,7 +64,7 @@ Based on usability testing (March 2026). Reworks the entire setup flow into a si | E | Contextual prompts for skipped steps (products, checkout, order detail) | 2h | planned | | F | Dashboard checklist and messaging rework | 2h | planned | | G | Coming soon page fixes (logo layout, admin login link) | 30m | done | -| H | External links UX (new tabs, icons, aria labels) | 1h | planned | +| H | External links UX (new tabs, icons, aria labels) | 1h | done | | I | Input styling — WCAG AA/AAA compliance | 1h | done | ### Notification system overhaul ([plan](docs/plans/notification-overhaul.md)) diff --git a/assets/css/admin/components.css b/assets/css/admin/components.css index 48bc79e..bf05c9a 100644 --- a/assets/css/admin/components.css +++ b/assets/css/admin/components.css @@ -4774,6 +4774,14 @@ .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ml-1 { margin-inline-start: 0.25rem; } +.external-link-icon { + width: 0.75em; + height: 0.75em; + margin-inline-start: 0.25em; + vertical-align: baseline; + opacity: 0.6; +} + .sr-only { position: absolute; width: 1px; diff --git a/assets/css/shop/utilities.css b/assets/css/shop/utilities.css index 99abe18..ac7d94a 100644 --- a/assets/css/shop/utilities.css +++ b/assets/css/shop/utilities.css @@ -14,6 +14,14 @@ border-width: 0; } + .external-link-icon { + width: 0.75em; + height: 0.75em; + margin-inline-start: 0.25em; + vertical-align: baseline; + opacity: 0.6; + } + .truncate { overflow: hidden; text-overflow: ellipsis; diff --git a/lib/berrypod_web/components/core_components.ex b/lib/berrypod_web/components/core_components.ex index dc9ce0c..9125aeb 100644 --- a/lib/berrypod_web/components/core_components.ex +++ b/lib/berrypod_web/components/core_components.ex @@ -423,6 +423,25 @@ defmodule BerrypodWeb.CoreComponents do """ end + @doc """ + Renders a link to an external site with proper security attributes, + an external-link icon, and screen reader context. + """ + attr :href, :string, required: true + attr :class, :string, default: nil + attr :rest, :global + slot :inner_block, required: true + + def external_link(assigns) do + ~H""" + + {render_slot(@inner_block)} + <.icon name="hero-arrow-top-right-on-square" class="external-link-icon" /> + (opens in new tab) + + """ + end + ## JS Commands def show(js \\ %JS{}, selector) do @@ -608,9 +627,10 @@ defmodule BerrypodWeb.CoreComponents do :if={@option[:url]} href={@option.url} target="_blank" - rel="noopener" + rel="noopener noreferrer" class="card-radio-link" onclick="event.stopPropagation();" + aria-label={@option.name <> " (opens in new tab)"} > {@option.name} ↗ diff --git a/lib/berrypod_web/components/shop_components/content.ex b/lib/berrypod_web/components/shop_components/content.ex index 5d3bbad..b696e2c 100644 --- a/lib/berrypod_web/components/shop_components/content.ex +++ b/lib/berrypod_web/components/shop_components/content.ex @@ -431,6 +431,7 @@ defmodule BerrypodWeb.ShopComponents.Content do <.social_icon platform={link.platform} /> {link.label} + (opens in new tab) <% end %> diff --git a/lib/berrypod_web/live/admin/email_settings.ex b/lib/berrypod_web/live/admin/email_settings.ex index 2fd4dd9..ffd27ce 100644 --- a/lib/berrypod_web/live/admin/email_settings.ex +++ b/lib/berrypod_web/live/admin/email_settings.ex @@ -272,8 +272,9 @@ defmodule BerrypodWeb.Admin.EmailSettings do :if={adapter.url} href={adapter.url} target="_blank" - rel="noopener" + rel="noopener noreferrer" class="admin-link-subtle admin-adapter-link" + aria-label={adapter.name <> " website (opens in new tab)"} > ↗ diff --git a/lib/berrypod_web/live/admin/order_show.ex b/lib/berrypod_web/live/admin/order_show.ex index b9e1f5a..7a405fd 100644 --- a/lib/berrypod_web/live/admin/order_show.ex +++ b/lib/berrypod_web/live/admin/order_show.ex @@ -134,15 +134,13 @@ defmodule BerrypodWeb.Admin.OrderShow do > <.icon name="hero-truck-mini" class="size-4" /> {@order.tracking_number} - - Track shipment → - + Track shipment + <.order_timeline entries={@timeline} /> diff --git a/lib/berrypod_web/live/admin/product_show.ex b/lib/berrypod_web/live/admin/product_show.ex index 079981e..1d82f76 100644 --- a/lib/berrypod_web/live/admin/product_show.ex +++ b/lib/berrypod_web/live/admin/product_show.ex @@ -102,11 +102,12 @@ defmodule BerrypodWeb.Admin.ProductShow do :if={provider_edit_url(@product)} href={provider_edit_url(@product)} target="_blank" - rel="noopener" + rel="noopener noreferrer" class="admin-btn admin-btn-ghost admin-btn-sm" > Edit on {provider_label(@product)} <.icon name="hero-arrow-top-right-on-square-mini" class="size-4" /> + (opens in new tab) <.link navigate={~p"/products/#{@product.slug}"} diff --git a/lib/berrypod_web/live/admin/providers/form.html.heex b/lib/berrypod_web/live/admin/providers/form.html.heex index a6b6541..a170d7c 100644 --- a/lib/berrypod_web/live/admin/providers/form.html.heex +++ b/lib/berrypod_web/live/admin/providers/form.html.heex @@ -15,10 +15,10 @@

Get your API key from {@provider.name}:

  1. - + <.external_link href={@provider.login_url} class="admin-link"> Log in to {@provider.name} - - (or create a free account) + + (or <.external_link href={@provider.signup_url} class="admin-link">create a free account)
  2. {raw(step)} diff --git a/lib/berrypod_web/live/admin/settings.ex b/lib/berrypod_web/live/admin/settings.ex index ca1a9c8..8eb1c28 100644 --- a/lib/berrypod_web/live/admin/settings.ex +++ b/lib/berrypod_web/live/admin/settings.ex @@ -592,14 +592,9 @@ defmodule BerrypodWeb.Admin.Settings do

    To accept payments, connect your Stripe account by entering your secret key. You can find it in your - + <.external_link href="https://dashboard.stripe.com/apikeys" class="admin-link"> Stripe dashboard - + under Developers → API keys.

    diff --git a/lib/berrypod_web/live/setup/onboarding.ex b/lib/berrypod_web/live/setup/onboarding.ex index c3f3cfc..3f4402a 100644 --- a/lib/berrypod_web/live/setup/onboarding.ex +++ b/lib/berrypod_web/live/setup/onboarding.ex @@ -392,9 +392,9 @@ defmodule BerrypodWeb.Setup.Onboarding do <% provider_info = Enum.find(@providers, &(&1.type == @selected)) %>

    {provider_info.setup_hint}. - - Open {provider_info.name} → - + <.external_link href={provider_info.setup_url} class="setup-link"> + Open {provider_info.name} +

    <.form for={@form} phx-submit="connect_provider"> @@ -428,14 +428,9 @@ defmodule BerrypodWeb.Setup.Onboarding do

    Enter your Stripe secret key to accept payments. - - Open Stripe dashboard → - + <.external_link href="https://dashboard.stripe.com/apikeys" class="setup-link"> + Open Stripe dashboard +

    <.form for={@form} phx-submit="connect_stripe"> diff --git a/lib/berrypod_web/page_renderer.ex b/lib/berrypod_web/page_renderer.ex index 9664fa5..07a70dd 100644 --- a/lib/berrypod_web/page_renderer.ex +++ b/lib/berrypod_web/page_renderer.ex @@ -888,6 +888,7 @@ defmodule BerrypodWeb.PageRenderer do target="_blank" rel="noopener noreferrer" class="order-detail-tracking-btn themed-button" + aria-label="Track parcel (opens in new tab)" > Track parcel