fix(tasks): open the task by clicking its text, links carved out

The first cut put an open-button overlay over the row, which intercepted
every click on the title text — nine smoke tests (and real users) open a
task by clicking its text, and the overlay stole those clicks. Make the
body a role=button instead, with each link stopping its own click from
bubbling: plain text opens the task, a link opens the page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kristianwind
2026-08-27 16:33:30 +02:00
co-authored by Claude Opus 4.8
parent 9d4a13c2c2
commit 4103e1b0ca
2 changed files with 27 additions and 37 deletions
+3 -3
View File
@@ -101,9 +101,9 @@ test('en URL i en opgave bliver et klikbart link, og resten åbner opgaven', asy
await expect(link).toHaveAttribute('href', 'https://example.com/artikel');
await expect(link).toHaveAttribute('rel', /noopener/);
// Clicking anywhere but the link still opens the task — the open-overlay sits
// behind the text, the link above it.
await row.getByRole('button', { name: /læs https/ }).click();
// Clicking the plain part of the title still opens the task; only the link is
// carved out. Aimed at the top-left, over "læs", not the URL in the middle.
await row.locator('.content').click({ position: { x: 3, y: 3 } });
await expect(page.getByRole('complementary', { name: 'Opgave' })).toBeVisible();
expect(trouble).toEqual([]);
+24 -34
View File
@@ -101,25 +101,30 @@
</button>
<!-- A URL pasted into the title or the description is clickable, without the
text ever having been anything but plain text. The link cannot live inside
the open-button — an <a> in a <button> is invalid — so the whole body is a
container and the button is an overlay behind it: plain text (below it)
still opens the task, links (raised above it) open the page. -->
<div class="body">
<!-- Opening the detail drawer is the default. `onedit` is for the callers
that want the click to mean something else. -->
<button
class="open"
aria-label={task.content}
onclick={() => (onedit ? onedit(task) : app.openDetail(task.id))}
></button>
text ever having been anything but plain text. The link is a real <a>, which
an <a> in a <button> may not be — so the body is a role=button rather than a
button, and each link stops its click from also opening the task. Clicking
any plain text still opens it. -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="body"
role="button"
tabindex="0"
onclick={() => (onedit ? onedit(task) : app.openDetail(task.id))}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onedit ? onedit(task) : app.openDetail(task.id);
}
}}
>
<span class="content"
>{#each linkify(task.content) as run}{#if run.href}<a
class="tlink"
href={run.href}
target="_blank"
rel="noopener noreferrer">{run.text}</a
rel="noopener noreferrer"
onclick={(e) => e.stopPropagation()}>{run.text}</a
>{:else}{run.text}{/if}{/each}</span
>
@@ -129,7 +134,8 @@
class="tlink"
href={run.href}
target="_blank"
rel="noopener noreferrer">{run.text}</a
rel="noopener noreferrer"
onclick={(e) => e.stopPropagation()}>{run.text}</a
>{:else}{run.text}{/if}{/each}</span
>
{/if}
@@ -327,31 +333,17 @@
}
.body {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
text-align: left;
min-width: 0;
}
/* The click target for opening the task, stretched behind everything. Plain
text and the meta row sit at the base level, so a click on them lands here;
only the links are raised above it. */
.open {
position: absolute;
inset: 0;
z-index: 1;
cursor: pointer;
background: none;
border: 0;
padding: 0;
margin: 0;
border-radius: var(--radius-sm);
}
.open:focus-visible {
.body:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
@@ -362,11 +354,9 @@
overflow-wrap: anywhere;
}
/* Raised above the open-overlay so the link takes the click, while the rest of
the line still opens the task. */
/* A real link, so it can be middle-clicked, copied and previewed. Its own click
is stopped from bubbling, so opening the page does not also open the task. */
.tlink {
position: relative;
z-index: 2;
color: var(--accent);
text-decoration: underline;
text-decoration-thickness: 1px;