️(frontend) improve version history list accessibility

Dynamic aria-label per version, aria-pressed + live region
This commit is contained in:
Cyril
2026-03-19 14:04:59 +01:00
parent 9991820cb1
commit ef108227b3
4 changed files with 75 additions and 52 deletions
+1
View File
@@ -9,6 +9,7 @@ and this project adheres to
### Changed
- 💫(frontend) fix the help button to the bottom in tree #2073
- ♿️(frontend) improve version history list accessibility #2033
## [v4.8.2] - 2026-03-19
@@ -25,7 +25,7 @@ test.describe('Doc Version', () => {
await expect(page.getByText('History', { exact: true })).toBeVisible();
const modal = page.getByRole('dialog', { name: 'Version history' });
const panel = modal.getByLabel('version list');
const panel = modal.getByLabel('Version list');
await expect(panel).toBeVisible();
await expect(modal.getByText('No versions')).toBeVisible();
@@ -155,11 +155,11 @@ test.describe('Doc Version', () => {
await getMenuItem(page, 'Version history').click();
const modal = page.getByRole('dialog', { name: 'Version history' });
const panel = modal.getByLabel('version list');
const panel = modal.getByLabel('Version list');
await expect(panel).toBeVisible();
await expect(page.getByText('History', { exact: true })).toBeVisible();
await panel.getByRole('button', { name: 'version item' }).click();
await panel.locator('.version-item').first().click();
await expect(modal.getByText('World')).toBeHidden();
@@ -1,7 +1,8 @@
import dynamic from 'next/dynamic';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text } from '@/components';
import { Box, BoxButton, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Doc } from '@/docs/doc-management';
@@ -18,54 +19,69 @@ const ModalConfirmationVersion = dynamic(
interface VersionItemProps {
docId: Doc['id'];
text: string;
versionId?: Versions['version_id'];
isActive: boolean;
onSelect?: () => void;
}
export const VersionItem = ({
docId,
versionId,
text,
isActive,
onSelect,
}: VersionItemProps) => {
const { t } = useTranslation();
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
return (
<>
<Box
<BoxButton
aria-label={t('Restore version of {{date}}', { date: text })}
aria-pressed={isActive}
$width="100%"
as="li"
$background={isActive ? colorsTokens['gray-100'] : 'transparent'}
$radius={spacingsTokens['3xs']}
$css={`
cursor: pointer;
&:hover {
background: ${colorsTokens['gray-100']};
&:focus-visible {
background: var(--c--globals--colors--gray-100);
border-radius: var(--c--globals--spacings--st);
}
`}
$hasTransition
$minWidth="13rem"
className="--docs--version-item"
className="version-item"
onClick={onSelect}
>
<Box
$padding={{ vertical: '0.7rem', horizontal: 'small' }}
$align="center"
$direction="row"
$justify="space-between"
$width="100%"
as="span"
$background={isActive ? colorsTokens['gray-100'] : 'transparent'}
$radius={spacingsTokens['3xs']}
$css={`
cursor: pointer;
&:hover {
background: ${colorsTokens['gray-100']};
}
`}
$hasTransition
$minWidth="13rem"
className="--docs--version-item"
>
<Box $direction="row" $gap="0.5rem" $align="center">
<Text $weight="bold" $size="sm">
{text}
</Text>
<Box
$padding={{ vertical: '0.7rem', horizontal: 'small' }}
$align="center"
$direction="row"
$justify="space-between"
$width="100%"
>
<Box $direction="row" $gap="0.5rem" $align="center">
<Text $weight="bold" $size="sm">
{text}
</Text>
</Box>
</Box>
</Box>
</Box>
</BoxButton>
{isModalVersionOpen && versionId && (
<ModalConfirmationVersion
onClose={() => setIsModalVersionOpen(false)}
@@ -3,14 +3,7 @@ import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import { APIError } from '@/api';
import {
Box,
BoxButton,
Icon,
InfiniteScroll,
Text,
TextErrors,
} from '@/components';
import { Box, Icon, InfiniteScroll, Text, TextErrors } from '@/components';
import { Doc } from '@/docs/doc-management';
import { useDate } from '@/hooks';
@@ -49,23 +42,24 @@ const VersionListState = ({
return (
<Box $gap="10px" $padding="xs">
{versions?.map((version) => (
<BoxButton
aria-label="version item"
className="version-item"
key={version.version_id}
onClick={() => {
onSelectVersion?.(version.version_id);
}}
>
<VersionItem
versionId={version.version_id}
text={formatDate(version.last_modified, DateTime.DATETIME_MED)}
docId={doc.id}
isActive={version.version_id === selectedVersionId}
/>
</BoxButton>
))}
{versions?.map((version) => {
const formattedDate = formatDate(
version.last_modified,
DateTime.DATETIME_MED,
);
const isSelected = version.version_id === selectedVersionId;
return (
<Box as="li" key={version.version_id} $css="list-style: none;">
<VersionItem
versionId={version.version_id}
text={formattedDate}
docId={doc.id}
isActive={isSelected}
onSelect={() => onSelectVersion?.(version.version_id)}
/>
</Box>
);
})}
{error && (
<Box
$justify="center"
@@ -97,6 +91,7 @@ export const VersionList = ({
selectedVersionId,
}: VersionListProps) => {
const { t } = useTranslation();
const { formatDate } = useDate();
const {
data,
@@ -112,6 +107,12 @@ export const VersionList = ({
const versions = data?.pages.reduce((acc, page) => {
return acc.concat(page.versions);
}, [] as Versions[]);
const selectedVersion = versions?.find(
(version) => version.version_id === selectedVersionId,
);
const selectedVersionDate = selectedVersion
? formatDate(selectedVersion.last_modified, DateTime.DATETIME_MED)
: null;
return (
<Box
@@ -127,7 +128,7 @@ export const VersionList = ({
as="ul"
$padding="none"
$margin={{ top: 'none' }}
role="listbox"
role="list"
>
{versions?.length === 0 && (
<Box $align="center" $margin="large">
@@ -145,6 +146,11 @@ export const VersionList = ({
selectedVersionId={selectedVersionId}
/>
</InfiniteScroll>
<Text className="sr-only" aria-live="polite">
{selectedVersionDate
? t('Selected version {{date}}', { date: selectedVersionDate })
: ''}
</Text>
</Box>
);
};