(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,48 +100,57 @@ export const DocsGrid = ({
)} )}
{hasDocs && ( {hasDocs && (
<Box $gap="6px" $overflow="auto"> <Box $gap="6px" $overflow="auto">
<Box <Box role="grid">
$direction="row" <Box role="rowgroup">
$padding={{ horizontal: 'xs' }} <Box
$gap="10px" $direction="row"
data-testid="docs-grid-header" $padding={{ horizontal: 'xs' }}
> $gap="10px"
<Box $flex={flexLeft} $padding="3xs"> data-testid="docs-grid-header"
<Text $size="xs" $variation="600" $weight="500"> role="row"
{t('Name')} >
</Text> <Box $flex={flexLeft} $padding="3xs" role="columnheader">
</Box> <Text $size="xs" $variation="600" $weight="500">
{isDesktop && ( {t('Name')}
<Box $flex={flexRight} $padding={{ vertical: '3xs' }}> </Text>
<Text $size="xs" $weight="500" $variation="600"> </Box>
{t('Updated at')} {isDesktop && (
</Text> <Box
$flex={flexRight}
$padding={{ vertical: '3xs' }}
role="columnheader"
>
<Text $size="xs" $weight="500" $variation="600">
{t('Updated at')}
</Text>
</Box>
)}
</Box> </Box>
</Box>
<Box role="rowgroup">
{isDesktop ? (
<DraggableDocGridContentList docs={docs} />
) : (
<DocGridContentList docs={docs} />
)}
</Box>
{hasNextPage && !loading && (
<InView
data-testid="infinite-scroll-trigger"
as="div"
onChange={loadMore}
>
{!isFetching && hasNextPage && (
<Button
onClick={() => void fetchNextPage()}
color="primary-text"
>
{t('More docs')}
</Button>
)}
</InView>
)} )}
</Box> </Box>
{isDesktop ? (
<DraggableDocGridContentList docs={docs} />
) : (
<DocGridContentList docs={docs} />
)}
{hasNextPage && !loading && (
<InView
data-testid="infinite-scroll-trigger"
as="div"
onChange={loadMore}
>
{!isFetching && hasNextPage && (
<Button
onClick={() => void fetchNextPage()}
color="primary-text"
>
{t('More docs')}
</Button>
)}
</InView>
)}
</Box> </Box>
)} )}
</Card> </Card>

View File

@@ -53,67 +53,76 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
`} `}
className="--docs--doc-grid-item" className="--docs--doc-grid-item"
> >
<StyledLink <Box
$flex={flexLeft}
role="gridcell"
$css={css` $css={css`
flex: ${flexLeft};
align-items: center; align-items: center;
min-width: 0; min-width: 0;
`} `}
href={`/docs/${doc.id}`}
> >
<Box <StyledLink
data-testid={`docs-grid-name-${doc.id}`} $css={css`
$direction="row" width: 100%;
$align="center" align-items: center;
$gap={spacingsTokens.xs} min-width: 0;
$padding={{ right: isDesktop ? 'md' : '3xs' }} `}
$maxWidth="100%" href={`/docs/${doc.id}`}
> >
<SimpleDocItem isPinned={doc.is_favorite} doc={doc} /> <Box
{isShared && ( data-testid={`docs-grid-name-${doc.id}`}
<Box $direction="row"
$padding={{ top: !isDesktop ? '4xs' : undefined }} $align="center"
$css={ $gap={spacingsTokens.xs}
!isDesktop $padding={{ right: isDesktop ? 'md' : '3xs' }}
? css` $maxWidth="100%"
align-self: flex-start; >
` <SimpleDocItem isPinned={doc.is_favorite} doc={doc} />
: undefined {isShared && (
} <Box
> $padding={{ top: !isDesktop ? '4xs' : undefined }}
{dragMode && ( $css={
<Icon !isDesktop
$theme="greyscale" ? css`
$variation="600" align-self: flex-start;
$size="14px" `
iconName={isPublic ? 'public' : 'vpn_lock'} : undefined
/> }
)} >
{!dragMode && ( {dragMode && (
<Tooltip <Icon
content={ $theme="greyscale"
<Text $textAlign="center" $variation="000"> $variation="600"
{isPublic $size="14px"
? t('Accessible to anyone') iconName={isPublic ? 'public' : 'vpn_lock'}
: t('Accessible to authenticated users')} />
</Text> )}
} {!dragMode && (
placement="top" <Tooltip
> content={
<div> <Text $textAlign="center" $variation="000">
<Icon {isPublic
$theme="greyscale" ? t('Accessible to anyone')
$variation="600" : t('Accessible to authenticated users')}
$size="14px" </Text>
iconName={isPublic ? 'public' : 'vpn_lock'} }
/> placement="top"
</div> >
</Tooltip> <div>
)} <Icon
</Box> $theme="greyscale"
)} $variation="600"
</Box> $size="14px"
</StyledLink> iconName={isPublic ? 'public' : 'vpn_lock'}
/>
</div>
</Tooltip>
)}
</Box>
)}
</Box>
</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>
<InfiniteScroll <Box>
as="ul" <Box as="ul" $padding="none">
hasMore={docs.hasNextPage} {favoriteDocs.map((doc) => (
isLoading={docs.isFetchingNextPage} <LeftPanelFavoriteItem key={doc.id} doc={doc} />
next={() => void docs.fetchNextPage()} ))}
$padding="none" </Box>
> {docs.hasNextPage && (
{favoriteDocs.map((doc) => ( <InfiniteScroll
<LeftPanelFavoriteItem key={doc.id} doc={doc} /> hasMore={docs.hasNextPage}
))} isLoading={docs.isFetchingNextPage}
</InfiniteScroll> next={() => void docs.fetchNextPage()}
$padding="none"
/>
)}
</Box>
</Box> </Box>
</Box> </Box>
); );