add external link UX: icons, rel attributes, screen reader labels
New external_link component in core_components handles target="_blank", rel="noopener noreferrer", external-link icon, and sr-only "(opens in new tab)" text. Migrated admin providers form, settings (Stripe), order tracking, onboarding setup links to use it. Fixed rel="noopener" to "noopener noreferrer" on remaining links (email settings, product show, core_components card radio). Added sr-only text to shop social link cards and aria-label to page renderer tracking link. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
696843bacd
commit
156a23da16
@ -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 |
|
| E | Contextual prompts for skipped steps (products, checkout, order detail) | 2h | planned |
|
||||||
| F | Dashboard checklist and messaging rework | 2h | planned |
|
| F | Dashboard checklist and messaging rework | 2h | planned |
|
||||||
| G | Coming soon page fixes (logo layout, admin login link) | 30m | done |
|
| 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 |
|
| I | Input styling — WCAG AA/AAA compliance | 1h | done |
|
||||||
|
|
||||||
### Notification system overhaul ([plan](docs/plans/notification-overhaul.md))
|
### Notification system overhaul ([plan](docs/plans/notification-overhaul.md))
|
||||||
|
|||||||
@ -4774,6 +4774,14 @@
|
|||||||
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.ml-1 { margin-inline-start: 0.25rem; }
|
.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 {
|
.sr-only {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
|||||||
@ -14,6 +14,14 @@
|
|||||||
border-width: 0;
|
border-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.external-link-icon {
|
||||||
|
width: 0.75em;
|
||||||
|
height: 0.75em;
|
||||||
|
margin-inline-start: 0.25em;
|
||||||
|
vertical-align: baseline;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.truncate {
|
.truncate {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|||||||
@ -423,6 +423,25 @@ defmodule BerrypodWeb.CoreComponents do
|
|||||||
"""
|
"""
|
||||||
end
|
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"""
|
||||||
|
<a href={@href} target="_blank" rel="noopener noreferrer" class={@class} {@rest}>
|
||||||
|
{render_slot(@inner_block)}
|
||||||
|
<.icon name="hero-arrow-top-right-on-square" class="external-link-icon" />
|
||||||
|
<span class="sr-only">(opens in new tab)</span>
|
||||||
|
</a>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
## JS Commands
|
## JS Commands
|
||||||
|
|
||||||
def show(js \\ %JS{}, selector) do
|
def show(js \\ %JS{}, selector) do
|
||||||
@ -608,9 +627,10 @@ defmodule BerrypodWeb.CoreComponents do
|
|||||||
:if={@option[:url]}
|
:if={@option[:url]}
|
||||||
href={@option.url}
|
href={@option.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener noreferrer"
|
||||||
class="card-radio-link"
|
class="card-radio-link"
|
||||||
onclick="event.stopPropagation();"
|
onclick="event.stopPropagation();"
|
||||||
|
aria-label={@option.name <> " (opens in new tab)"}
|
||||||
>
|
>
|
||||||
{@option.name} ↗
|
{@option.name} ↗
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -431,6 +431,7 @@ defmodule BerrypodWeb.ShopComponents.Content do
|
|||||||
<.social_icon platform={link.platform} />
|
<.social_icon platform={link.platform} />
|
||||||
</span>
|
</span>
|
||||||
<span>{link.label}</span>
|
<span>{link.label}</span>
|
||||||
|
<span class="sr-only">(opens in new tab)</span>
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -272,8 +272,9 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
|||||||
:if={adapter.url}
|
:if={adapter.url}
|
||||||
href={adapter.url}
|
href={adapter.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener noreferrer"
|
||||||
class="admin-link-subtle admin-adapter-link"
|
class="admin-link-subtle admin-adapter-link"
|
||||||
|
aria-label={adapter.name <> " website (opens in new tab)"}
|
||||||
>
|
>
|
||||||
↗
|
↗
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -134,15 +134,13 @@ defmodule BerrypodWeb.Admin.OrderShow do
|
|||||||
>
|
>
|
||||||
<span class="admin-text-secondary"><.icon name="hero-truck-mini" class="size-4" /></span>
|
<span class="admin-text-secondary"><.icon name="hero-truck-mini" class="size-4" /></span>
|
||||||
<span class="admin-text-medium">{@order.tracking_number}</span>
|
<span class="admin-text-medium">{@order.tracking_number}</span>
|
||||||
<a
|
<.external_link
|
||||||
:if={@order.tracking_url not in [nil, ""]}
|
:if={@order.tracking_url not in [nil, ""]}
|
||||||
href={@order.tracking_url}
|
href={@order.tracking_url}
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
class="admin-link"
|
class="admin-link"
|
||||||
>
|
>
|
||||||
Track shipment →
|
Track shipment
|
||||||
</a>
|
</.external_link>
|
||||||
</div>
|
</div>
|
||||||
<.order_timeline entries={@timeline} />
|
<.order_timeline entries={@timeline} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -102,11 +102,12 @@ defmodule BerrypodWeb.Admin.ProductShow do
|
|||||||
:if={provider_edit_url(@product)}
|
:if={provider_edit_url(@product)}
|
||||||
href={provider_edit_url(@product)}
|
href={provider_edit_url(@product)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener noreferrer"
|
||||||
class="admin-btn admin-btn-ghost admin-btn-sm"
|
class="admin-btn admin-btn-ghost admin-btn-sm"
|
||||||
>
|
>
|
||||||
Edit on {provider_label(@product)}
|
Edit on {provider_label(@product)}
|
||||||
<.icon name="hero-arrow-top-right-on-square-mini" class="size-4" />
|
<.icon name="hero-arrow-top-right-on-square-mini" class="size-4" />
|
||||||
|
<span class="sr-only">(opens in new tab)</span>
|
||||||
</.link>
|
</.link>
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/products/#{@product.slug}"}
|
navigate={~p"/products/#{@product.slug}"}
|
||||||
|
|||||||
@ -15,10 +15,10 @@
|
|||||||
<p class="admin-callout-title">Get your API key from {@provider.name}:</p>
|
<p class="admin-callout-title">Get your API key from {@provider.name}:</p>
|
||||||
<ol class="admin-callout-list">
|
<ol class="admin-callout-list">
|
||||||
<li>
|
<li>
|
||||||
<a href={@provider.login_url} target="_blank" rel="noopener" class="admin-link">
|
<.external_link href={@provider.login_url} class="admin-link">
|
||||||
Log in to {@provider.name}
|
Log in to {@provider.name}
|
||||||
</a>
|
</.external_link>
|
||||||
(or <a href={@provider.signup_url} target="_blank" rel="noopener" class="admin-link">create a free account</a>)
|
(or <.external_link href={@provider.signup_url} class="admin-link">create a free account</.external_link>)
|
||||||
</li>
|
</li>
|
||||||
<li :for={step <- @provider.setup_steps}>
|
<li :for={step <- @provider.setup_steps}>
|
||||||
{raw(step)}
|
{raw(step)}
|
||||||
|
|||||||
@ -592,14 +592,9 @@ defmodule BerrypodWeb.Admin.Settings do
|
|||||||
<p class="admin-section-desc admin-section-desc-flush">
|
<p class="admin-section-desc admin-section-desc-flush">
|
||||||
To accept payments, connect your Stripe account by entering your secret key.
|
To accept payments, connect your Stripe account by entering your secret key.
|
||||||
You can find it in your
|
You can find it in your
|
||||||
<a
|
<.external_link href="https://dashboard.stripe.com/apikeys" class="admin-link">
|
||||||
href="https://dashboard.stripe.com/apikeys"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
class="admin-link"
|
|
||||||
>
|
|
||||||
Stripe dashboard
|
Stripe dashboard
|
||||||
</a>
|
</.external_link>
|
||||||
under Developers → API keys.
|
under Developers → API keys.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@ -392,9 +392,9 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
<% provider_info = Enum.find(@providers, &(&1.type == @selected)) %>
|
<% provider_info = Enum.find(@providers, &(&1.type == @selected)) %>
|
||||||
<p class="setup-hint">
|
<p class="setup-hint">
|
||||||
{provider_info.setup_hint}.
|
{provider_info.setup_hint}.
|
||||||
<a href={provider_info.setup_url} target="_blank" rel="noopener" class="setup-link">
|
<.external_link href={provider_info.setup_url} class="setup-link">
|
||||||
Open {provider_info.name} →
|
Open {provider_info.name}
|
||||||
</a>
|
</.external_link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<.form for={@form} phx-submit="connect_provider">
|
<.form for={@form} phx-submit="connect_provider">
|
||||||
@ -428,14 +428,9 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
<div>
|
<div>
|
||||||
<p class="setup-hint">
|
<p class="setup-hint">
|
||||||
Enter your Stripe secret key to accept payments.
|
Enter your Stripe secret key to accept payments.
|
||||||
<a
|
<.external_link href="https://dashboard.stripe.com/apikeys" class="setup-link">
|
||||||
href="https://dashboard.stripe.com/apikeys"
|
Open Stripe dashboard
|
||||||
target="_blank"
|
</.external_link>
|
||||||
rel="noopener"
|
|
||||||
class="setup-link"
|
|
||||||
>
|
|
||||||
Open Stripe dashboard →
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<.form for={@form} phx-submit="connect_stripe">
|
<.form for={@form} phx-submit="connect_stripe">
|
||||||
|
|||||||
@ -888,6 +888,7 @@ defmodule BerrypodWeb.PageRenderer do
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="order-detail-tracking-btn themed-button"
|
class="order-detail-tracking-btn themed-button"
|
||||||
|
aria-label="Track parcel (opens in new tab)"
|
||||||
>
|
>
|
||||||
Track parcel
|
Track parcel
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user