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
|
<a
|
||||||
href={rowHref}
|
href={rowHref}
|
||||||
data-variant={variant}
|
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
|
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' : ''}"
|
{variant === 'for-you' ? 'border-l-[3px] border-accent bg-accent-bg/10' : ''}"
|
||||||
>
|
>
|
||||||
@@ -159,20 +162,11 @@ const rowHref: string = $derived(
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if variant === 'comment'}
|
{#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
|
<p
|
||||||
data-testid="chronik-comment-preview"
|
data-testid="chronik-comment-preview"
|
||||||
class="mt-1 line-clamp-1 font-serif text-sm text-ink-2 italic sm:line-clamp-2"
|
class="mt-1 line-clamp-1 font-serif text-sm text-ink-2 italic sm:line-clamp-2"
|
||||||
>
|
>
|
||||||
„…“
|
{item.commentPreview ?? '„…"'}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,49 @@ describe('ChronikRow', () => {
|
|||||||
expect(link).not.toBeNull();
|
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 ---
|
// --- robustness: title rendering for edge cases ---
|
||||||
it('still renders the row link when documentTitle is an empty string', async () => {
|
it('still renders the row link when documentTitle is an empty string', async () => {
|
||||||
// Felix: verbText.indexOf(docTitle) returned 0 for empty titles — the span
|
// 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.
|
* @description Annotation associated with the comment; populated only for COMMENT_ADDED and MENTION_CREATED kinds.
|
||||||
*/
|
*/
|
||||||
annotationId?: string;
|
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: {
|
InvitePrefillDTO: {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user