🗣️ a11y: Add Screen Reader Context to Conversation Date Group Headings (#12340)

* fix: add screen reader-only context for convo date groupings

Otherwise, the screen reader simply says something like "today" or
"previous 7 days" without any other context, which is confusing (especially
since this is a heading, so theoretically something you'd navigate to
directly).

Visually it's identical to before, but screen readers have added context
now.

* fix: move a11y key to com_a11y_* namespace and add DateLabel test

Move screen-reader-only translation key from com_ui_* to com_a11y_*
namespace where it belongs, and add test coverage to prevent silent
accessibility regressions.

---------

Co-authored-by: Dan Lew <daniel@mightyacorn.com>
This commit is contained in:
Danny Avila 2026-03-20 16:53:26 -04:00 committed by GitHub
parent 6976414464
commit 676d297cb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 60 additions and 0 deletions

View file

@ -99,6 +99,9 @@ const DateLabel: FC<{ groupName: string; isFirst?: boolean }> = memo(({ groupNam
const localize = useLocalize();
return (
<h2
aria-label={localize('com_a11y_chats_date_section', {
date: localize(groupName as TranslationKeys) || groupName,
})}
className={cn('pl-1 pt-1 text-text-secondary', isFirst === true ? 'mt-0' : 'mt-2')}
style={{ fontSize: '0.7rem' }}
>
@ -394,4 +397,5 @@ const Conversations: FC<ConversationsProps> = ({
);
};
export { DateLabel };
export default memo(Conversations);

View file

@ -0,0 +1,55 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { DateLabel } from '../Conversations';
jest.mock('~/hooks', () => ({
useLocalize: () => (key: string, params?: Record<string, string>) => {
const translations: Record<string, string> = {
com_a11y_chats_date_section: `Chats from ${params?.date ?? ''}`,
com_ui_date_today: 'Today',
com_ui_date_yesterday: 'Yesterday',
com_ui_date_previous_7_days: 'Previous 7 days',
};
return translations[key] ?? key;
},
}));
describe('DateLabel', () => {
it('provides accessible heading name via aria-label', () => {
render(<DateLabel groupName="com_ui_date_today" />);
expect(screen.getByRole('heading', { level: 2, name: 'Chats from Today' })).toBeInTheDocument();
});
it('renders visible text as the localized group name', () => {
render(<DateLabel groupName="com_ui_date_today" />);
expect(screen.getByText('Today')).toBeInTheDocument();
});
it('sets aria-label with the full accessible phrase', () => {
const { container } = render(<DateLabel groupName="com_ui_date_yesterday" />);
const heading = container.querySelector('h2');
expect(heading).toHaveAttribute('aria-label', 'Chats from Yesterday');
});
it('uses raw groupName for unrecognized translation keys', () => {
render(<DateLabel groupName="Unknown Group" />);
expect(
screen.getByRole('heading', { level: 2, name: 'Chats from Unknown Group' }),
).toBeInTheDocument();
});
it('applies mt-0 for the first date header', () => {
const { container } = render(<DateLabel groupName="com_ui_date_today" isFirst={true} />);
const heading = container.querySelector('h2');
expect(heading).toHaveClass('mt-0');
expect(heading).not.toHaveClass('mt-2');
});
it('applies mt-2 for non-first date headers', () => {
const { container } = render(<DateLabel groupName="com_ui_date_today" isFirst={false} />);
const heading = container.querySelector('h2');
expect(heading).toHaveClass('mt-2');
expect(heading).not.toHaveClass('mt-0');
});
});

View file

@ -2,6 +2,7 @@
"chat_direction_left_to_right": "Left to Right",
"chat_direction_right_to_left": "Right to Left",
"com_a11y_ai_composing": "The AI is still composing.",
"com_a11y_chats_date_section": "Chats from {{date}}",
"com_a11y_end": "The AI has finished their reply.",
"com_a11y_selected": "selected",
"com_a11y_start": "The AI has started their reply.",