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:
co-authored by
Claude Opus 4.8
parent
9d4a13c2c2
commit
4103e1b0ca
@@ -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([]);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user