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('href', 'https://example.com/artikel');
await expect(link).toHaveAttribute('rel', /noopener/); await expect(link).toHaveAttribute('rel', /noopener/);
// Clicking anywhere but the link still opens the task — the open-overlay sits // Clicking the plain part of the title still opens the task; only the link is
// behind the text, the link above it. // carved out. Aimed at the top-left, over "læs", not the URL in the middle.
await row.getByRole('button', { name: /læs https/ }).click(); await row.locator('.content').click({ position: { x: 3, y: 3 } });
await expect(page.getByRole('complementary', { name: 'Opgave' })).toBeVisible(); await expect(page.getByRole('complementary', { name: 'Opgave' })).toBeVisible();
expect(trouble).toEqual([]); expect(trouble).toEqual([]);
+24 -34
View File
@@ -101,25 +101,30 @@
</button> </button>
<!-- A URL pasted into the title or the description is clickable, without the <!-- 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 text ever having been anything but plain text. The link is a real <a>, which
the open-button — an <a> in a <button> is invalid — so the whole body is a an <a> in a <button> may not be — so the body is a role=button rather than a
container and the button is an overlay behind it: plain text (below it) button, and each link stops its click from also opening the task. Clicking
still opens the task, links (raised above it) open the page. --> any plain text still opens it. -->
<div class="body"> <!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- Opening the detail drawer is the default. `onedit` is for the callers <div
that want the click to mean something else. --> class="body"
<button role="button"
class="open" tabindex="0"
aria-label={task.content} onclick={() => (onedit ? onedit(task) : app.openDetail(task.id))}
onclick={() => (onedit ? onedit(task) : app.openDetail(task.id))} onkeydown={(e) => {
></button> if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onedit ? onedit(task) : app.openDetail(task.id);
}
}}
>
<span class="content" <span class="content"
>{#each linkify(task.content) as run}{#if run.href}<a >{#each linkify(task.content) as run}{#if run.href}<a
class="tlink" class="tlink"
href={run.href} href={run.href}
target="_blank" target="_blank"
rel="noopener noreferrer">{run.text}</a rel="noopener noreferrer"
onclick={(e) => e.stopPropagation()}>{run.text}</a
>{:else}{run.text}{/if}{/each}</span >{:else}{run.text}{/if}{/each}</span
> >
@@ -129,7 +134,8 @@
class="tlink" class="tlink"
href={run.href} href={run.href}
target="_blank" target="_blank"
rel="noopener noreferrer">{run.text}</a rel="noopener noreferrer"
onclick={(e) => e.stopPropagation()}>{run.text}</a
>{:else}{run.text}{/if}{/each}</span >{:else}{run.text}{/if}{/each}</span
> >
{/if} {/if}
@@ -327,31 +333,17 @@
} }
.body { .body {
position: relative;
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
text-align: left; text-align: left;
min-width: 0; 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; cursor: pointer;
background: none;
border: 0;
padding: 0;
margin: 0;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} }
.open:focus-visible { .body:focus-visible {
outline: 2px solid var(--accent); outline: 2px solid var(--accent);
outline-offset: 2px; outline-offset: 2px;
} }
@@ -362,11 +354,9 @@
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
/* Raised above the open-overlay so the link takes the click, while the rest of /* A real link, so it can be middle-clicked, copied and previewed. Its own click
the line still opens the task. */ is stopped from bubbling, so opening the page does not also open the task. */
.tlink { .tlink {
position: relative;
z-index: 2;
color: var(--accent); color: var(--accent);
text-decoration: underline; text-decoration: underline;
text-decoration-thickness: 1px; text-decoration-thickness: 1px;