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:
12
frontend/src/lib/timeline/GlyphLabel.svelte
Normal file
12
frontend/src/lib/timeline/GlyphLabel.svelte
Normal 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>
|
||||||
15
frontend/src/lib/timeline/GlyphLabel.svelte.spec.ts
Normal file
15
frontend/src/lib/timeline/GlyphLabel.svelte.spec.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user