fix(web): restore message center CTA (#6894)

* fix(web): restore message center CTA

* fix(web): unify expanded message card surface
This commit is contained in:
lefarcen
2026-08-14 10:07:42 +00:00
committed by GitHub
parent d0df45d4a8
commit cc34d2d409
3 changed files with 62 additions and 3 deletions
@@ -244,6 +244,11 @@
box-shadow: inset 2px 0 0 var(--accent);
}
.itemExpanded {
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--accent) 5%, transparent);
}
.itemSummary {
box-sizing: border-box;
width: 100%;
@@ -267,6 +272,10 @@
background: color-mix(in srgb, var(--accent) 5%, transparent);
}
.itemExpanded .itemSummary:hover {
background: transparent;
}
.itemMeta {
min-width: 0;
max-width: 100%;
@@ -329,6 +338,33 @@
white-space: normal;
}
.itemActions {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0 13px 12px;
}
.itemActions .primaryAction {
min-height: 30px;
padding: 0 12px;
border: 1px solid color-mix(in srgb, var(--accent) 72%, var(--border));
border-radius: var(--radius-sm);
background: var(--accent);
color: var(--accent-contrast, #fff);
cursor: pointer;
font-size: 12px;
font-weight: 600;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.14);
white-space: nowrap;
}
.itemActions .primaryAction:hover {
border-color: color-mix(in srgb, var(--accent) 72%, var(--border));
background: color-mix(in srgb, var(--accent) 86%, var(--text-strong));
color: var(--accent-contrast, #fff);
}
.empty {
min-height: 260px;
display: flex;
+12
View File
@@ -294,7 +294,19 @@ function MessageItem({
}) {
const [expanded, setExpanded] = useState(false);
const formatted = formatPublishedDate(message.publishedAt, locale);
const ctaUrl = safeExternalUrl(message.ctaUrl);
return <article className={`${styles.item}${message.readAt ? '' : ` ${styles.itemUnread}`}${expanded ? ` ${styles.itemExpanded}` : ''}`}>
<button type="button" className={styles.itemSummary} aria-expanded={expanded} onClick={() => { setExpanded((value) => !value); void onRead(message.id).catch(onError); }}><span className={styles.itemMeta}><span>{message.typeName}</span>{formatted ? <time dateTime={message.publishedAt}>{formatted}</time> : null}</span><strong>{message.title}</strong><span className={styles.bodyPreview}>{message.body}</span></button>
{expanded && message.ctaLabel && ctaUrl ? <div className={styles.itemActions}><button type="button" className={styles.primaryAction} onClick={() => window.open(ctaUrl, '_blank', 'noopener,noreferrer')}>{message.ctaLabel}</button></div> : null}
</article>;
}
function safeExternalUrl(value: string | null): string | null {
if (!value) return null;
try {
const parsed = new URL(value, window.location.href);
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.toString() : null;
} catch {
return null;
}
}
@@ -130,13 +130,13 @@ describe('MessageCenter', () => {
await waitFor(() => expect(screen.getByText('All caught up')).toBeTruthy());
});
it('expands the whole message row without rendering view actions', async () => {
it('expands the whole message row and opens its CTA', async () => {
const open = vi.spyOn(window, 'open').mockImplementation(() => null);
renderMessageCenter();
await openCenter();
const row = screen.getByRole('button', { name: /Open Design 0\.14 is available/ });
expect(row).toHaveAttribute('aria-expanded', 'false');
expect(screen.queryByText('View')).toBeNull();
expect(screen.queryByRole('button', { name: 'View update' })).toBeNull();
fireEvent.click(row);
@@ -144,7 +144,8 @@ describe('MessageCenter', () => {
expect(row).toHaveAttribute('aria-expanded', 'true');
expect(row.closest('article')?.className).toContain('itemExpanded');
expect(screen.getByText('The new release is ready.')).toBeTruthy();
expect(screen.queryByRole('button', { name: 'View update' })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'View update' }));
expect(open).toHaveBeenCalledWith('https://open-design.ai/update', '_blank', 'noopener,noreferrer');
});
it('keeps both anonymous reads when two expands resolve out of order', async () => {
@@ -475,6 +476,16 @@ describe('MessageCenter', () => {
expect(readAttempts).toBe(1);
});
it('hides CTA actions for non-http URLs', async () => {
mockFetch({
messages: [{ ...defaultMessages[0]!, ctaUrl: 'javascript:alert(1)' }],
});
renderMessageCenter();
await openCenter();
fireEvent.click(screen.getByRole('button', { name: /Open Design 0\.14 is available/ }));
expect(screen.queryByRole('button', { name: 'View update' })).toBeNull();
});
it('closes with Escape and restores trigger focus', async () => {
renderMessageCenter();
const trigger = screen.getByTestId('message-center-trigger');