fix(chronik): split Für-dich row markup — Dismiss is sibling of link, not nested

HTML5 forbids interactive content (<button>, <a>, <input>...) as descendants
of <a>. The original <a href=…><button>✓</button></a> markup triggered two
concrete bugs flagged by Felix, Nora, and Leonie in PR #288 review:

- Browsers inconsistently route the nested click: on some engines the
  stopPropagation() still bubbles, and the user navigates into the document
  instead of dismissing.
- The senior audience (60+) tap-selects with a slight drag, and the OS
  treats the interaction as anchor vs. button inconsistently — a
  reproducible usability failure Leonie has seen in testing before.

Refactor to the Option-C layout from issue #285 comment #3573: outer <li>
flex container, <a> wrapping avatar + body + time, <button> as a sibling.
Independent focus stops, invalid-HTML gone, no behavioural regression.

A new spec locks the invariant: `dismiss.closest('a')` must be null.

Part of #285, address PR #288 review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-20 18:05:28 +02:00
committed by marcel
parent 089a1d063a
commit 58ea2f827a
2 changed files with 36 additions and 26 deletions

View File

@@ -113,4 +113,17 @@ describe('ChronikFuerDichBox', () => {
expect(onMarkRead).toHaveBeenCalledTimes(1);
expect(onMarkRead.mock.calls[0][0]).toEqual(n);
});
it('Dismiss button is a sibling of the document link, never nested inside <a>', async () => {
render(ChronikFuerDichBox, {
unread: [notif({ id: 'x' })],
onMarkRead: vi.fn(),
onMarkAllRead: vi.fn()
});
const dismiss = document.querySelector('[data-testid="chronik-fuerdich-dismiss"]');
expect(dismiss).not.toBeNull();
// HTML spec forbids interactive content descendants of <a>.
// Prevents the senior-audience tap-drag bug flagged by Leonie.
expect(dismiss?.closest('a')).toBeNull();
});
});