(frontend) fix major accessibility issues found by wave and axe

improves a11y by fixing multiple critical validation errors

Signed-off-by: Cyril <c.gromoff@gmail.com>
This commit is contained in:
Cyril
2025-09-05 16:01:15 +02:00
parent 1d20a8b0a7
commit cd84751cb9
13 changed files with 155 additions and 125 deletions

View File

@@ -20,6 +20,9 @@ and this project adheres to
- 🔒️(backend) configure throttle on every viewsets #1343 - 🔒️(backend) configure throttle on every viewsets #1343
- ⬆️ Bump eslint to V9 #1071 - ⬆️ Bump eslint to V9 #1071
- ♿(frontend) improve accessibility:
- ♿(frontend) fix major accessibility issues reported by wave and axe #1344
- #1341
## [3.6.0] - 2025-09-04 ## [3.6.0] - 2025-09-04

View File

@@ -226,9 +226,13 @@ test.describe('Doc Editor', () => {
await editor.fill('Hello World Doc persisted 2'); await editor.fill('Hello World Doc persisted 2');
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible(); await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
await page.waitForTimeout(1000);
const urlDoc = page.url(); const urlDoc = page.url();
await page.goto(urlDoc); await page.goto(urlDoc);
// Wait for editor to load
await expect(editor).toBeVisible();
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible(); await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
}); });

View File

@@ -80,9 +80,7 @@ test.describe('Documents Grid mobile', () => {
hasText: 'My mocked document', hasText: 'My mocked document',
}); });
await expect( await expect(row.getByTestId('doc-title')).toHaveText('My mocked document');
row.locator('[aria-describedby="doc-title"]').nth(0),
).toHaveText('My mocked document');
}); });
}); });
@@ -295,7 +293,7 @@ test.describe('Documents Grid', () => {
docs = result.results as SmallDoc[]; docs = result.results as SmallDoc[];
await expect(page.getByTestId('grid-loader')).toBeHidden(); await expect(page.getByTestId('grid-loader')).toBeHidden();
await expect(page.locator('h4').getByText('All docs')).toBeVisible(); await expect(page.locator('h2').getByText('All docs')).toBeVisible();
const thead = page.getByTestId('docs-grid-header'); const thead = page.getByTestId('docs-grid-header');
await expect(thead.getByText(/Name/i)).toBeVisible(); await expect(thead.getByText(/Name/i)).toBeVisible();

View File

@@ -173,12 +173,13 @@ test.describe('Document search', () => {
.getByRole('combobox', { name: 'Quick search input' }) .getByRole('combobox', { name: 'Quick search input' })
.fill('sub page search'); .fill('sub page search');
// Expect to find the first doc // Expect to find the first and second docs in the results list
const resultsList = page.getByRole('listbox');
await expect( await expect(
page.getByRole('presentation').getByLabel(firstDocTitle), resultsList.getByRole('option', { name: firstDocTitle }),
).toBeVisible(); ).toBeVisible();
await expect( await expect(
page.getByRole('presentation').getByLabel(secondDocTitle), resultsList.getByRole('option', { name: secondDocTitle }),
).toBeVisible(); ).toBeVisible();
await page.getByRole('button', { name: 'close' }).click(); await page.getByRole('button', { name: 'close' }).click();
@@ -195,14 +196,15 @@ test.describe('Document search', () => {
.fill('second'); .fill('second');
// Now there is a sub page - expect to have the focus on the current doc // Now there is a sub page - expect to have the focus on the current doc
const updatedResultsList = page.getByRole('listbox');
await expect( await expect(
page.getByRole('presentation').getByLabel(secondDocTitle), updatedResultsList.getByRole('option', { name: secondDocTitle }),
).toBeVisible(); ).toBeVisible();
await expect( await expect(
page.getByRole('presentation').getByLabel(secondChildDocTitle), updatedResultsList.getByRole('option', { name: secondChildDocTitle }),
).toBeVisible(); ).toBeVisible();
await expect( await expect(
page.getByRole('presentation').getByLabel(firstDocTitle), updatedResultsList.getByRole('option', { name: firstDocTitle }),
).toBeHidden(); ).toBeHidden();
}); });
}); });

View File

@@ -172,7 +172,7 @@ export const goToGridDoc = async (
await expect(row).toBeVisible(); await expect(row).toBeVisible();
const docTitleContent = row.locator('[aria-describedby="doc-title"]').first(); const docTitleContent = row.getByTestId('doc-title').first();
const docTitle = await docTitleContent.textContent(); const docTitle = await docTitleContent.textContent();
expect(docTitle).toBeDefined(); expect(docTitle).toBeDefined();

View File

@@ -110,7 +110,6 @@ export const DropdownMenu = ({
$direction="row" $direction="row"
$align="center" $align="center"
$position="relative" $position="relative"
aria-controls="menu"
> >
<Box>{children}</Box> <Box>{children}</Box>
<Icon <Icon
@@ -125,9 +124,7 @@ export const DropdownMenu = ({
/> />
</Box> </Box>
) : ( ) : (
<Box ref={blockButtonRef} aria-controls="menu"> <Box ref={blockButtonRef}>{children}</Box>
{children}
</Box>
) )
} }
> >

View File

@@ -82,12 +82,11 @@ export const SimpleDocItem = ({
</Box> </Box>
<Box $justify="center" $overflow="auto"> <Box $justify="center" $overflow="auto">
<Text <Text
aria-describedby="doc-title"
aria-label={doc.title || untitledDocument}
$size="sm" $size="sm"
$variation="1000" $variation="1000"
$weight="500" $weight="500"
$css={ItemTextCss} $css={ItemTextCss}
data-testid="doc-title"
> >
{displayTitle} {displayTitle}
</Text> </Text>

View File

@@ -70,7 +70,6 @@ export const DocsGrid = ({
> >
<DocsGridLoader isLoading={isRefetching || loading} /> <DocsGridLoader isLoading={isRefetching || loading} />
<Card <Card
role="grid"
data-testid="docs-grid" data-testid="docs-grid"
$height="100%" $height="100%"
$width="100%" $width="100%"
@@ -84,7 +83,7 @@ export const DocsGrid = ({
}} }}
> >
<Text <Text
as="h4" as="h2"
$size="h4" $size="h4"
$variation="1000" $variation="1000"
$margin={{ top: '0px', bottom: '10px' }} $margin={{ top: '0px', bottom: '10px' }}
@@ -101,32 +100,40 @@ export const DocsGrid = ({
)} )}
{hasDocs && ( {hasDocs && (
<Box $gap="6px" $overflow="auto"> <Box $gap="6px" $overflow="auto">
<Box role="grid">
<Box role="rowgroup">
<Box <Box
$direction="row" $direction="row"
$padding={{ horizontal: 'xs' }} $padding={{ horizontal: 'xs' }}
$gap="10px" $gap="10px"
data-testid="docs-grid-header" data-testid="docs-grid-header"
role="row"
> >
<Box $flex={flexLeft} $padding="3xs"> <Box $flex={flexLeft} $padding="3xs" role="columnheader">
<Text $size="xs" $variation="600" $weight="500"> <Text $size="xs" $variation="600" $weight="500">
{t('Name')} {t('Name')}
</Text> </Text>
</Box> </Box>
{isDesktop && ( {isDesktop && (
<Box $flex={flexRight} $padding={{ vertical: '3xs' }}> <Box
$flex={flexRight}
$padding={{ vertical: '3xs' }}
role="columnheader"
>
<Text $size="xs" $weight="500" $variation="600"> <Text $size="xs" $weight="500" $variation="600">
{t('Updated at')} {t('Updated at')}
</Text> </Text>
</Box> </Box>
)} )}
</Box> </Box>
</Box>
<Box role="rowgroup">
{isDesktop ? ( {isDesktop ? (
<DraggableDocGridContentList docs={docs} /> <DraggableDocGridContentList docs={docs} />
) : ( ) : (
<DocGridContentList docs={docs} /> <DocGridContentList docs={docs} />
)} )}
</Box>
{hasNextPage && !loading && ( {hasNextPage && !loading && (
<InView <InView
data-testid="infinite-scroll-trigger" data-testid="infinite-scroll-trigger"
@@ -144,6 +151,7 @@ export const DocsGrid = ({
</InView> </InView>
)} )}
</Box> </Box>
</Box>
)} )}
</Card> </Card>
</Box> </Box>

View File

@@ -52,10 +52,18 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
} }
`} `}
className="--docs--doc-grid-item" className="--docs--doc-grid-item"
>
<Box
$flex={flexLeft}
role="gridcell"
$css={css`
align-items: center;
min-width: 0;
`}
> >
<StyledLink <StyledLink
$css={css` $css={css`
flex: ${flexLeft}; width: 100%;
align-items: center; align-items: center;
min-width: 0; min-width: 0;
`} `}
@@ -114,6 +122,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
)} )}
</Box> </Box>
</StyledLink> </StyledLink>
</Box>
<Box <Box
$flex={flexRight} $flex={flexRight}
@@ -121,6 +130,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
$align="center" $align="center"
$justify={isDesktop ? 'space-between' : 'flex-end'} $justify={isDesktop ? 'space-between' : 'flex-end'}
$gap="32px" $gap="32px"
role="gridcell"
> >
{isDesktop && ( {isDesktop && (
<StyledLink href={`/docs/${doc.id}`}> <StyledLink href={`/docs/${doc.id}`}>

View File

@@ -1,5 +1,5 @@
import { Loader } from '@openfun/cunningham-react'; import { Loader } from '@openfun/cunningham-react';
import { createGlobalStyle } from 'styled-components'; import { createGlobalStyle, css } from 'styled-components';
import { Box } from '@/components'; import { Box } from '@/components';
@@ -32,6 +32,9 @@ export const DocsGridLoader = ({ isLoading }: DocsGridLoaderProps) => {
$zIndex={998} $zIndex={998}
$position="absolute" $position="absolute"
className="--docs--doc-grid-loader" className="--docs--doc-grid-loader"
$css={css`
pointer-events: none;
`}
> >
<Loader /> <Loader />
</Box> </Box>

View File

@@ -19,6 +19,7 @@ export const Draggable = <T,>(props: DraggableProps<T>) => {
{...attributes} {...attributes}
data-testid={`draggable-doc-${props.id}`} data-testid={`draggable-doc-${props.id}`}
className="--docs--grid-draggable" className="--docs--grid-draggable"
role="presentation"
> >
{props.children} {props.children}
</div> </div>

View File

@@ -35,6 +35,7 @@ export const Droppable = ({
<Box <Box
ref={setNodeRef} ref={setNodeRef}
data-testid={`droppable-doc-${id}`} data-testid={`droppable-doc-${id}`}
role="presentation"
$css={css` $css={css`
border-radius: 4px; border-radius: 4px;
background-color: ${enableHover background-color: ${enableHover

View File

@@ -44,17 +44,21 @@ export const LeftPanelFavorites = () => {
> >
{t('Pinned documents')} {t('Pinned documents')}
</Text> </Text>
<Box>
<Box as="ul" $padding="none">
{favoriteDocs.map((doc) => (
<LeftPanelFavoriteItem key={doc.id} doc={doc} />
))}
</Box>
{docs.hasNextPage && (
<InfiniteScroll <InfiniteScroll
as="ul"
hasMore={docs.hasNextPage} hasMore={docs.hasNextPage}
isLoading={docs.isFetchingNextPage} isLoading={docs.isFetchingNextPage}
next={() => void docs.fetchNextPage()} next={() => void docs.fetchNextPage()}
$padding="none" $padding="none"
> />
{favoriteDocs.map((doc) => ( )}
<LeftPanelFavoriteItem key={doc.id} doc={doc} /> </Box>
))}
</InfiniteScroll>
</Box> </Box>
</Box> </Box>
); );