refactor(timeline): extract a shared GlyphLabel primitive

The aria-hidden glyph + sr-only label markup was hand-copied in LetterCard
and YearLetterStrip. Extract a small GlyphLabel component and use it at
both sites so the accessibility idiom has a single owner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-14 13:48:25 +02:00
parent 0bd6790b1f
commit 0a235dc911
4 changed files with 31 additions and 4 deletions

View File

@@ -0,0 +1,12 @@
<script lang="ts">
/**
* A decorative glyph paired with a screen-reader-only text label — the
* non-color accessibility cue used across timeline cards (e.g. the ✉ on a
* letter title or letter-count). The glyph is `aria-hidden`; the label carries
* the meaning for assistive tech.
*/
let { glyph, label }: { glyph: string; label: string } = $props();
</script>
<span aria-hidden="true">{glyph}</span>
<span class="sr-only">{label}</span>

View File

@@ -0,0 +1,15 @@
import { describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import GlyphLabel from './GlyphLabel.svelte';
afterEach(() => cleanup());
describe('GlyphLabel', () => {
it('renders the glyph aria-hidden with an sr-only label sibling', () => {
render(GlyphLabel, { glyph: '✉', label: 'Brief' });
const hidden = document.querySelector('[aria-hidden="true"]');
expect(hidden?.textContent).toBe('✉');
const srOnly = document.querySelector('.sr-only');
expect(srOnly?.textContent).toBe('Brief');
});
});

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import * as m from '$lib/paraglide/messages.js'; import * as m from '$lib/paraglide/messages.js';
import { timelineDateLabel } from './dateLabel'; import { timelineDateLabel } from './dateLabel';
import GlyphLabel from './GlyphLabel.svelte';
import type { components } from '$lib/generated/api'; import type { components } from '$lib/generated/api';
type TimelineEntryDTO = components['schemas']['TimelineEntryDTO']; type TimelineEntryDTO = components['schemas']['TimelineEntryDTO'];
@@ -33,8 +34,7 @@ const receiver = $derived(
interpolated into the escaped user title; the title keeps its own interpolated into the escaped user title; the title keeps its own
pre-line span for multi-line OCR text (REQ-008/016/021). --> pre-line span for multi-line OCR text (REQ-008/016/021). -->
<span class="font-serif text-sm font-bold break-words text-ink"> <span class="font-serif text-sm font-bold break-words text-ink">
<span aria-hidden="true"></span> <GlyphLabel glyph="✉" label={m.timeline_letter_glyph_label()} />
<span class="sr-only">{m.timeline_letter_glyph_label()}</span>
<span class="whitespace-pre-line">{entry.title}</span> <span class="whitespace-pre-line">{entry.title}</span>
</span> </span>
{/if} {/if}

View File

@@ -3,6 +3,7 @@ import * as m from '$lib/paraglide/messages.js';
import { getLocale } from '$lib/paraglide/runtime.js'; import { getLocale } from '$lib/paraglide/runtime.js';
import { formatTickLabel } from '$lib/shared/utils/monthBuckets'; import { formatTickLabel } from '$lib/shared/utils/monthBuckets';
import Sparkline from '$lib/shared/primitives/Sparkline.svelte'; import Sparkline from '$lib/shared/primitives/Sparkline.svelte';
import GlyphLabel from './GlyphLabel.svelte';
import LetterCard from './LetterCard.svelte'; import LetterCard from './LetterCard.svelte';
import { monthHistogram } from './timelineDensity'; import { monthHistogram } from './timelineDensity';
import type { components } from '$lib/generated/api'; import type { components } from '$lib/generated/api';
@@ -30,8 +31,7 @@ const dezLabel = $derived(formatTickLabel(`${year}-12`, getLocale()));
<div class="mx-auto max-w-md rounded-sm border border-line bg-surface p-3 shadow-sm"> <div class="mx-auto max-w-md rounded-sm border border-line bg-surface p-3 shadow-sm">
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<span class="font-sans text-sm font-bold text-brand-navy"> <span class="font-sans text-sm font-bold text-brand-navy">
<span aria-hidden="true"></span> <GlyphLabel glyph="✉" label={m.timeline_letter_glyph_label()} />
<span class="sr-only">{m.timeline_letter_glyph_label()}</span>
{m.timeline_letters_count({ count: letters.length })} {m.timeline_letters_count({ count: letters.length })}
</span> </span>
<button <button