ux(rover-planning): table rows are real <a href> links

Each path-table cell now wraps its content in <.link navigate={...}>,
so the row behaves as a native anchor: hovering shows the destination
URL in the browser status bar, right-click 'open in new tab' works,
middle-click works, and the rendered HTML is meaningful to assistive
tech / scrapers / link-validators.

Replaces the phx-click={JS.navigate(...)} pattern, which gave none of
those affordances and required the user to trust that the row was
clickable. Whole-row hover highlight is preserved via a CSS
:has(a:hover) selector so cell-hover still tints the row.
This commit is contained in:
Graham McIntire 2026-05-03 16:20:45 -05:00
parent 893a02e1e2
commit 9bec5721d0
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59

View file

@ -14,7 +14,6 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
alias Microwaveprop.RoverPlanning alias Microwaveprop.RoverPlanning
alias Microwaveprop.RoverPlanning.Mission alias Microwaveprop.RoverPlanning.Mission
alias MicrowavepropWeb.LocationResolver alias MicrowavepropWeb.LocationResolver
alias Phoenix.LiveView.JS
@impl true @impl true
def mount(%{"id" => id}, _session, socket) do def mount(%{"id" => id}, _session, socket) do
@ -329,6 +328,29 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
defp path_url(_, _), do: nil defp path_url(_, _), do: nil
# Wraps a single table cell's content in a real <a> when an href is
# available, so the browser shows the destination on hover and
# right-click / middle-click work natively. Falls back to a plain
# <div> when the row has no path (e.g. compute hasn't started).
attr :href, :string, default: nil
slot :inner_block, required: true
defp path_cell(%{href: href} = assigns) when is_binary(href) do
~H"""
<.link navigate={@href} class="block px-3 py-2 text-inherit no-underline">
{render_slot(@inner_block)}
</.link>
"""
end
defp path_cell(assigns) do
~H"""
<div class="px-3 py-2">
{render_slot(@inner_block)}
</div>
"""
end
@impl true @impl true
def render(assigns) do def render(assigns) do
summary = progress_summary(assigns.paths) summary = progress_summary(assigns.paths)
@ -513,38 +535,58 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<%!-- Each cell wraps its content in <.link>, so the row
behaves as a real anchor: native href hover preview,
right-click "open in new tab", middle-click, etc. The
row-hover highlight is preserved via :has(a:hover) so
hovering any cell still tints the whole row. --%>
<tr <tr
:for={path <- group_paths} :for={path <- group_paths}
phx-click={path_url(@mission, path) && JS.navigate(path_url(@mission, path))}
class={[ class={[
path_url(@mission, path) && "cursor-pointer hover:bg-base-200" path_url(@mission, path) &&
"[&:has(a:hover)]:bg-base-200 transition-colors"
]} ]}
title={path_url(@mission, path) && "Open in Path Calculator"}
> >
<td class="font-mono text-xs"> <td class="font-mono text-xs p-0">
<% {primary, secondary} = station_lines(path) %> <% {primary, secondary} = station_lines(path) %>
<div>{primary}</div> <.path_cell href={path_url(@mission, path)}>
<div :if={secondary} class="opacity-60">{secondary}</div> <div>{primary}</div>
<div :if={secondary} class="opacity-60">{secondary}</div>
</.path_cell>
</td> </td>
<td class="font-mono text-xs"> <td class="font-mono text-xs p-0">
{band_label(path.band_mhz || @mission.band_mhz)} <.path_cell href={path_url(@mission, path)}>
{band_label(path.band_mhz || @mission.band_mhz)}
</.path_cell>
</td> </td>
<td>{status_badge(path.status)}</td> <td class="p-0">
<td class="text-xs"> <.path_cell href={path_url(@mission, path)}>
{if path.result, do: format_distance(path.result["distance_km"]), else: ""} {status_badge(path.status)}
</.path_cell>
</td> </td>
<td class="text-xs"> <td class="text-xs p-0">
{if path.result, <.path_cell href={path_url(@mission, path)}>
do: format_loss(path.result["total_baseline_loss_db"]), {if path.result, do: format_distance(path.result["distance_km"]), else: ""}
else: ""} </.path_cell>
</td> </td>
<td> <td class="text-xs p-0">
{if path.result, do: verdict_badge(path.result["verdict"]), else: ""} <.path_cell href={path_url(@mission, path)}>
{if path.result,
do: format_loss(path.result["total_baseline_loss_db"]),
else: ""}
</.path_cell>
</td> </td>
<td> <td class="p-0">
{if path.result, <.path_cell href={path_url(@mission, path)}>
do: propagation_badge(path.result["propagation_score"]), {if path.result, do: verdict_badge(path.result["verdict"]), else: ""}
else: ""} </.path_cell>
</td>
<td class="p-0">
<.path_cell href={path_url(@mission, path)}>
{if path.result,
do: propagation_badge(path.result["propagation_score"]),
else: ""}
</.path_cell>
</td> </td>
</tr> </tr>
</tbody> </tbody>