feat(chronik): render commentPreview in ChronikRow; add aria-label for screen readers
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m49s
CI / OCR Service Tests (push) Successful in 45s
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 3m37s
CI / OCR Service Tests (pull_request) Successful in 48s
CI / Backend Unit Tests (pull_request) Failing after 3m21s
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m49s
CI / OCR Service Tests (push) Successful in 45s
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 3m37s
CI / OCR Service Tests (pull_request) Successful in 48s
CI / Backend Unit Tests (pull_request) Failing after 3m21s
Replace the „…" placeholder with {item.commentPreview ?? '„…"'}. Plain-text
binding — no {@html} — as specified in the security note from issue #285.
Adds aria-label to the <a> wrapper for COMMENT_ADDED rows that carry a preview,
giving screen reader users the full context in one announcement.
Generated api.ts updated manually to include commentPreview?:string; will be
regenerated by npm run generate:api once the backend is running.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -108,6 +108,9 @@ const rowHref: string = $derived(
|
||||
<a
|
||||
href={rowHref}
|
||||
data-variant={variant}
|
||||
aria-label={variant === 'comment' && item.commentPreview
|
||||
? `${actorName} ${m.chronik_comment_added({ actor: '', doc: docTitle }).trim()} — ${item.commentPreview}`
|
||||
: undefined}
|
||||
class="group flex items-start gap-3 p-3 transition-colors hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:outline-none
|
||||
{variant === 'for-you' ? 'border-l-[3px] border-accent bg-accent-bg/10' : ''}"
|
||||
>
|
||||
@@ -159,20 +162,11 @@ const rowHref: string = $derived(
|
||||
</p>
|
||||
|
||||
{#if variant === 'comment'}
|
||||
<!--
|
||||
TODO: the backend does not yet expose a comment body preview on
|
||||
ActivityFeedItemDTO. Render an ellipsis placeholder until it does —
|
||||
duplicating the document title here looks like the comment is
|
||||
quoting itself (Leonie, PR #288 review).
|
||||
SECURITY: once item.commentPreview lands, render via {text}, never
|
||||
{@html}. The backend must truncate and strip tags server-side (Nora,
|
||||
issue #285 comment #3552).
|
||||
-->
|
||||
<p
|
||||
data-testid="chronik-comment-preview"
|
||||
class="mt-1 line-clamp-1 font-serif text-sm text-ink-2 italic sm:line-clamp-2"
|
||||
>
|
||||
„…“
|
||||
{item.commentPreview ?? '„…"'}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -186,6 +186,49 @@ describe('ChronikRow', () => {
|
||||
expect(link).not.toBeNull();
|
||||
});
|
||||
|
||||
// --- commentPreview content ---
|
||||
it('renders commentPreview text when variant is comment and commentPreview is present', async () => {
|
||||
const item: ActivityFeedItemDTO = {
|
||||
...baseItem,
|
||||
kind: 'COMMENT_ADDED',
|
||||
commentPreview: 'Hello family, great letter!'
|
||||
};
|
||||
render(ChronikRow, { item });
|
||||
const preview = document.querySelector('[data-testid="chronik-comment-preview"]');
|
||||
expect(preview).not.toBeNull();
|
||||
expect(preview?.textContent).toContain('Hello family, great letter!');
|
||||
});
|
||||
|
||||
it('renders placeholder ellipsis when variant is comment and commentPreview is null', async () => {
|
||||
const item: ActivityFeedItemDTO = {
|
||||
...baseItem,
|
||||
kind: 'COMMENT_ADDED',
|
||||
commentPreview: undefined
|
||||
};
|
||||
render(ChronikRow, { item });
|
||||
const preview = document.querySelector('[data-testid="chronik-comment-preview"]');
|
||||
expect(preview).not.toBeNull();
|
||||
expect(preview?.textContent?.trim()).toBe('„…"');
|
||||
});
|
||||
|
||||
it('does not render preview paragraph for non-comment variants', async () => {
|
||||
const item: ActivityFeedItemDTO = { ...baseItem, kind: 'TEXT_SAVED' };
|
||||
render(ChronikRow, { item });
|
||||
expect(document.querySelector('[data-testid="chronik-comment-preview"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('link has aria-label containing preview text for comment variant with preview', async () => {
|
||||
const item: ActivityFeedItemDTO = {
|
||||
...baseItem,
|
||||
kind: 'COMMENT_ADDED',
|
||||
commentPreview: 'A wonderful letter from grandma'
|
||||
};
|
||||
render(ChronikRow, { item });
|
||||
const link = document.querySelector('a[aria-label]');
|
||||
expect(link).not.toBeNull();
|
||||
expect(link?.getAttribute('aria-label')).toContain('A wonderful letter from grandma');
|
||||
});
|
||||
|
||||
// --- robustness: title rendering for edge cases ---
|
||||
it('still renders the row link when documentTitle is an empty string', async () => {
|
||||
// Felix: verbText.indexOf(docTitle) returned 0 for empty titles — the span
|
||||
|
||||
@@ -2402,6 +2402,8 @@ export interface components {
|
||||
* @description Annotation associated with the comment; populated only for COMMENT_ADDED and MENTION_CREATED kinds.
|
||||
*/
|
||||
annotationId?: string;
|
||||
/** @description Plain-text preview of the comment body (HTML stripped server-side, truncated to 120 chars); null for non-comment feed items or deleted comments. */
|
||||
commentPreview?: string;
|
||||
};
|
||||
InvitePrefillDTO: {
|
||||
firstName: string;
|
||||
|
||||
Reference in New Issue
Block a user