Compare commits

...

3 Commits

Author SHA1 Message Date
Cyril 5cccc1ae6f (e2e) update tests for list semantics and add keyboard nav test
Adapt selectors from grid/row to list/listitem and add a Tab+Enter test
2026-03-25 18:03:57 +01:00
Cyril de9259f636 ️(frontend) localize dnd-kit screen reader instructions
Pass i18n strings for screenReaderInstructions and announcements.
2026-03-25 18:03:43 +01:00
Cyril 0f49230231 ️(frontend) replace ARIA grid pattern with list in docs grid
Use list/listitem roles, mark column headers as aria-hidden
2026-03-25 18:03:33 +01:00
7 changed files with 139 additions and 50 deletions
+1
View File
@@ -17,6 +17,7 @@ and this project adheres to
- ♿️(frontend) improve language picker accessibility #2069
- ♿️(frontend) add aria-hidden to decorative icons in dropdown menu #2093
- 🐛(backend) move lock table closer to the insert operation targeted
- ♿️(frontend) replace ARIA grid pattern with list in docs grid #2131
### Fixed
@@ -400,7 +400,7 @@ test.describe('Doc grid dnd mobile', () => {
await expect(page.getByTestId('docs-grid')).toBeVisible();
await expect(page.getByTestId('grid-loader')).toBeHidden();
await expect(docsGrid.getByRole('row').first()).toBeVisible();
await expect(docsGrid.getByRole('listitem').first()).toBeVisible();
await expect(docsGrid.locator('.--docs--grid-droppable')).toHaveCount(0);
await createDoc(page, 'Draggable doc mobile', browserName, 1, true);
@@ -76,7 +76,7 @@ test.describe('Documents Grid mobile', () => {
await expect(docsGrid).toBeVisible();
await expect(page.getByTestId('grid-loader')).toBeHidden();
const rows = docsGrid.getByRole('row');
const rows = docsGrid.getByRole('listitem');
const row = rows.filter({
hasText: 'My mocked document',
});
@@ -289,6 +289,29 @@ test.describe('Documents Grid', () => {
);
});
test('opens a document with keyboard (Tab + Enter)', async ({
page,
browserName,
}) => {
await page.goto('/');
const [docTitle] = await createDoc(page, 'keyboard-nav-test', browserName);
await page.goto('/');
await expect(page.getByTestId('grid-loader')).toBeHidden();
const row = await getGridRow(page, docTitle);
const link = row.getByRole('link').first();
await link.focus();
await expect(link).toBeFocused();
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/\/docs\//);
await verifyDocName(page, docTitle);
});
test('checks the infinite scroll', async ({ page }) => {
let docs: SmallDoc[];
const responsePromisePage1 = page.waitForResponse((response) => {
@@ -183,11 +183,11 @@ export const verifyDocName = async (page: Page, docName: string) => {
};
export const getGridRow = async (page: Page, title: string) => {
const docsGrid = page.getByRole('grid');
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
await expect(page.getByTestId('grid-loader')).toBeHidden();
const rows = docsGrid.getByRole('row');
const rows = docsGrid.getByRole('listitem');
const row = rows
.filter({
@@ -215,7 +215,7 @@ export const goToGridDoc = async (
await expect(docsGrid).toBeVisible();
await expect(page.getByTestId('grid-loader')).toBeHidden();
const rows = docsGrid.getByRole('row');
const rows = docsGrid.getByRole('listitem');
const row = title
? rows.filter({
@@ -1,4 +1,9 @@
import { DndContext, DragOverlay, Modifier } from '@dnd-kit/core';
import {
DndContext,
DragOverlay,
Modifier,
UniqueIdentifier,
} from '@dnd-kit/core';
import { getEventCoordinates } from '@dnd-kit/utilities';
import { useModal } from '@gouvfr-lasuite/cunningham-react';
import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit';
@@ -107,6 +112,59 @@ export const DraggableDocGridContentList = ({
const { t } = useTranslation();
const dndAccessibility = useMemo(
() => ({
screenReaderInstructions: {
draggable: t(
'To pick up a draggable item, press space or enter. While dragging, use the arrow keys to move the item. Press space or enter again to drop the item in its new position, or press escape to cancel.',
),
},
announcements: {
onDragStart({ active }: { active: { id: UniqueIdentifier } }) {
return t('Picked up document {{id}}.', { id: active.id });
},
onDragOver({
active,
over,
}: {
active: { id: UniqueIdentifier };
over: { id: UniqueIdentifier } | null;
}) {
if (over) {
return t('Document {{activeId}} is over document {{overId}}.', {
activeId: active.id,
overId: over.id,
});
}
return t('Document {{id}} is no longer over a droppable area.', {
id: active.id,
});
},
onDragEnd({
active,
over,
}: {
active: { id: UniqueIdentifier };
over: { id: UniqueIdentifier } | null;
}) {
if (over) {
return t(
'Document {{activeId}} was dropped over document {{overId}}.',
{ activeId: active.id, overId: over.id },
);
}
return t('Document {{id}} was dropped.', { id: active.id });
},
onDragCancel({ active }: { active: { id: UniqueIdentifier } }) {
return t('Dragging was cancelled. Document {{id}} was dropped.', {
id: active.id,
});
},
},
}),
[t],
);
const overlayText = useMemo(() => {
if (!canDrag) {
return t('You must be the owner to move the document');
@@ -147,6 +205,7 @@ export const DraggableDocGridContentList = ({
modifiers={[snapToTopLeft]}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
accessibility={dndAccessibility}
>
{docs.map((doc) => (
<DraggableDocGridItem
@@ -140,36 +140,30 @@ export const DocsGrid = ({
$overflow="auto"
$padding={{ vertical: 'sm', horizontal: isDesktop ? 'md' : 'xs' }}
>
<Box role="grid" aria-label={t('Documents grid')}>
<Box role="rowgroup">
<Box
$direction="row"
$padding={{ horizontal: 'xs' }}
$gap="10px"
data-testid="docs-grid-header"
role="row"
>
<Box $flex={flexLeft} $padding="3xs" role="columnheader">
<Text $size="xs" $variation="secondary" $weight="500">
{t('Name')}
<Box aria-label={t('Documents grid')}>
<Box
$direction="row"
$padding={{ horizontal: 'xs' }}
$gap="10px"
data-testid="docs-grid-header"
aria-hidden="true"
>
<Box $flex={flexLeft} $padding="3xs">
<Text $size="xs" $variation="secondary" $weight="500">
{t('Name')}
</Text>
</Box>
{isDesktop && (
<Box $flex={flexRight} $padding={{ vertical: '3xs' }}>
<Text $size="xs" $weight="500" $variation="secondary">
{DocDefaultFilter.TRASHBIN === target
? t('Days remaining')
: t('Updated at')}
</Text>
</Box>
{isDesktop && (
<Box
$flex={flexRight}
$padding={{ vertical: '3xs' }}
role="columnheader"
>
<Text $size="xs" $weight="500" $variation="secondary">
{DocDefaultFilter.TRASHBIN === target
? t('Days remaining')
: t('Updated at')}
</Text>
</Box>
)}
</Box>
)}
</Box>
<Box role="rowgroup">
<Box role="list">
<DocGridContentList docs={docs} />
</Box>
</Box>
@@ -32,6 +32,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
const { isDesktop } = useResponsiveStore();
const { flexLeft, flexRight } = useResponsiveDocGrid();
const { spacingsTokens } = useCunninghamTheme();
const dateToDisplay = useDateToDisplay(doc, isInTrashbin);
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
@@ -46,7 +47,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
$direction="row"
$width="100%"
$align="center"
role="row"
role="listitem"
$gap="20px"
$padding={{ vertical: '4xs', horizontal: isDesktop ? 'base' : 'xs' }}
$css={css`
@@ -65,7 +66,6 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
>
<Box
$flex={flexLeft}
role="gridcell"
$css={css`
align-items: center;
min-width: 0;
@@ -90,9 +90,15 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
$align="center"
$justify={isDesktop ? 'space-between' : 'flex-end'}
$gap="32px"
role="gridcell"
>
<StyledLink href={`/docs/${doc.id}`} tabIndex={-1}>
<StyledLink
href={`/docs/${doc.id}`}
tabIndex={-1}
aria-label={t('{{title}}, updated {{date}}', {
title: doc.title || untitledDocument,
date: dateToDisplay,
})}
>
<DocsGridItemDate
doc={doc}
isDesktop={isDesktop}
@@ -196,23 +202,11 @@ const IconPublic = ({ isPublic }: { isPublic: boolean }) => {
);
};
export const DocsGridItemDate = ({
doc,
isDesktop,
isInTrashbin,
}: {
doc: Doc;
isDesktop: boolean;
isInTrashbin: boolean;
}) => {
const useDateToDisplay = (doc: Doc, isInTrashbin: boolean) => {
const { data: config } = useConfig();
const { t } = useTranslation();
const { relativeDate, calculateDaysLeft } = useDate();
if (!isDesktop) {
return null;
}
let dateToDisplay = relativeDate(doc.updated_at);
if (isInTrashbin && config?.TRASHBIN_CUTOFF_DAYS && doc.deleted_at) {
@@ -224,6 +218,24 @@ export const DocsGridItemDate = ({
dateToDisplay = `${daysLeft} ${t('days', { count: daysLeft })}`;
}
return dateToDisplay;
};
export const DocsGridItemDate = ({
doc,
isDesktop,
isInTrashbin,
}: {
doc: Doc;
isDesktop: boolean;
isInTrashbin: boolean;
}) => {
const dateToDisplay = useDateToDisplay(doc, isInTrashbin);
if (!isDesktop) {
return null;
}
return (
<Text
$size="xs"